diff --git a/ProjectLighthouse.Servers.GameServer/Controllers/CommentController.cs b/ProjectLighthouse.Servers.GameServer/Controllers/CommentController.cs index 15d065440..c85247273 100644 --- a/ProjectLighthouse.Servers.GameServer/Controllers/CommentController.cs +++ b/ProjectLighthouse.Servers.GameServer/Controllers/CommentController.cs @@ -81,10 +81,7 @@ public async Task GetComments(string? username, string? slotType, if (targetId == 0) return this.NotFound(); - List blockedUsers = await ( - from blockedProfile in this.database.BlockedProfiles - where blockedProfile.UserId == token.UserId - select blockedProfile.BlockedUserId).ToListAsync(); + List blockedUsers = await this.database.GetBlockedUsers(token.UserId); List comments = (await this.database.Comments.Where(p => p.TargetId == targetId && p.Type == type) .OrderByDescending(p => p.Timestamp) diff --git a/ProjectLighthouse.Servers.Website/Controllers/CommentController.cs b/ProjectLighthouse.Servers.Website/Controllers/CommentController.cs new file mode 100644 index 000000000..d7c40b193 --- /dev/null +++ b/ProjectLighthouse.Servers.Website/Controllers/CommentController.cs @@ -0,0 +1,82 @@ +using LBPUnion.ProjectLighthouse.Configuration; +using LBPUnion.ProjectLighthouse.Database; +using LBPUnion.ProjectLighthouse.Helpers; +using LBPUnion.ProjectLighthouse.Logging; +using LBPUnion.ProjectLighthouse.Types.Entities.Profile; +using LBPUnion.ProjectLighthouse.Types.Entities.Token; +using LBPUnion.ProjectLighthouse.Types.Logging; +using Microsoft.AspNetCore.Mvc; + +namespace LBPUnion.ProjectLighthouse.Servers.Website.Controllers; + +[Route("{type}/{id:int}")] +public class CommentController : ControllerBase +{ + private readonly DatabaseContext database; + + public CommentController(DatabaseContext database) + { + this.database = database; + } + + private static CommentType? ParseType(string type) => + type switch + { + "slot" => CommentType.Level, + "user" => CommentType.Profile, + _ => null, + }; + + [HttpGet("rateComment")] + public async Task RateComment(string type, int id, [FromQuery] int? commentId, [FromQuery] int? rating, [FromQuery] string? redirect) + { + WebTokenEntity? token = this.database.WebTokenFromRequest(this.Request); + if (token == null) return this.Redirect("~/login"); + + CommentType? commentType = ParseType(type); + if (commentType == null) return this.BadRequest(); + + await this.database.RateComment(token.UserId, commentId.GetValueOrDefault(), rating.GetValueOrDefault()); + + return this.Redirect(redirect ?? $"~/user/{id}#{commentId}"); + } + + [HttpPost("postComment")] + public async Task PostComment(string type, int id, [FromForm] string? msg, [FromQuery] string? redirect) + { + WebTokenEntity? token = this.database.WebTokenFromRequest(this.Request); + if (token == null) return this.Redirect("~/login"); + + CommentType? commentType = ParseType(type); + if (commentType == null) return this.BadRequest(); + + if (msg == null) + { + Logger.Error($"Refusing to post comment from {token.UserId} on {commentType} {id}, {nameof(msg)} is null", + LogArea.Comments); + return this.Redirect("~/user/" + id); + } + + string username = await this.database.UsernameFromWebToken(token); + string filteredText = CensorHelper.FilterMessage(msg); + + if (ServerConfiguration.Instance.LogChatFiltering && filteredText != msg) + Logger.Info( + $"Censored profane word(s) from {commentType} comment sent by {username}: \"{msg}\" => \"{filteredText}\"", + LogArea.Filter); + + bool success = await this.database.PostComment(token.UserId, id, (CommentType)commentType, filteredText); + if (success) + { + Logger.Success($"Posted comment from {username}: \"{filteredText}\" on {commentType} {id}", + LogArea.Comments); + } + else + { + Logger.Error($"Failed to post comment from {username}: \"{filteredText}\" on {commentType} {id}", + LogArea.Comments); + } + + return this.Redirect(redirect ?? $"~/{type}/" + id); + } +} \ No newline at end of file diff --git a/ProjectLighthouse.Servers.Website/Controllers/SlotPageController.cs b/ProjectLighthouse.Servers.Website/Controllers/SlotPageController.cs index 66eafbc08..2db16becb 100644 --- a/ProjectLighthouse.Servers.Website/Controllers/SlotPageController.cs +++ b/ProjectLighthouse.Servers.Website/Controllers/SlotPageController.cs @@ -46,49 +46,6 @@ public async Task UnpublishSlot([FromRoute] int id) return this.Redirect("~/slots/0"); } - [HttpGet("rateComment")] - public async Task RateComment([FromRoute] int id, [FromQuery] int commentId, [FromQuery] int rating) - { - WebTokenEntity? token = this.database.WebTokenFromRequest(this.Request); - if (token == null) return this.Redirect("~/login"); - - await this.database.RateComment(token.UserId, commentId, rating); - - return this.Redirect($"~/slot/{id}#{commentId}"); - } - - [HttpPost("postComment")] - public async Task PostComment([FromRoute] int id, [FromForm] string? msg) - { - WebTokenEntity? token = this.database.WebTokenFromRequest(this.Request); - if (token == null) return this.Redirect("~/login"); - - if (msg == null) - { - Logger.Error($"Refusing to post comment from {token.UserId} on level {id}, {nameof(msg)} is null", LogArea.Comments); - return this.Redirect("~/slot/" + id); - } - - string username = await this.database.UsernameFromWebToken(token); - string filteredText = CensorHelper.FilterMessage(msg); - - if (ServerConfiguration.Instance.LogChatFiltering && filteredText != msg) - Logger.Info($"Censored profane word(s) from slot comment sent by {username}: \"{msg}\" => \"{filteredText}\"", - LogArea.Filter); - - bool success = await this.database.PostComment(token.UserId, id, CommentType.Level, filteredText); - if (success) - { - Logger.Success($"Posted comment from {username}: \"{filteredText}\" on level {id}", LogArea.Comments); - } - else - { - Logger.Error($"Failed to post comment from {username}: \"{filteredText}\" on level {id}", LogArea.Comments); - } - - return this.Redirect("~/slot/" + id); - } - [HttpGet("heart")] public async Task HeartLevel([FromRoute] int id, [FromQuery] string? callbackUrl) { diff --git a/ProjectLighthouse.Servers.Website/Controllers/UserPageController.cs b/ProjectLighthouse.Servers.Website/Controllers/UserPageController.cs index 161bb1a87..a124d3397 100644 --- a/ProjectLighthouse.Servers.Website/Controllers/UserPageController.cs +++ b/ProjectLighthouse.Servers.Website/Controllers/UserPageController.cs @@ -1,11 +1,7 @@ #nullable enable -using LBPUnion.ProjectLighthouse.Configuration; using LBPUnion.ProjectLighthouse.Database; -using LBPUnion.ProjectLighthouse.Helpers; -using LBPUnion.ProjectLighthouse.Logging; using LBPUnion.ProjectLighthouse.Types.Entities.Profile; using LBPUnion.ProjectLighthouse.Types.Entities.Token; -using LBPUnion.ProjectLighthouse.Types.Logging; using Microsoft.AspNetCore.Mvc; using Microsoft.EntityFrameworkCore; @@ -22,49 +18,6 @@ public UserPageController(DatabaseContext database) this.database = database; } - [HttpGet("rateComment")] - public async Task RateComment([FromRoute] int id, [FromQuery] int? commentId, [FromQuery] int? rating) - { - WebTokenEntity? token = this.database.WebTokenFromRequest(this.Request); - if (token == null) return this.Redirect("~/login"); - - await this.database.RateComment(token.UserId, commentId.GetValueOrDefault(), rating.GetValueOrDefault()); - - return this.Redirect($"~/user/{id}#{commentId}"); - } - - [HttpPost("postComment")] - public async Task PostComment([FromRoute] int id, [FromForm] string? msg) - { - WebTokenEntity? token = this.database.WebTokenFromRequest(this.Request); - if (token == null) return this.Redirect("~/login"); - - if (msg == null) - { - Logger.Error($"Refusing to post comment from {token.UserId} on user {id}, {nameof(msg)} is null", LogArea.Comments); - return this.Redirect("~/user/" + id); - } - - string username = await this.database.UsernameFromWebToken(token); - string filteredText = CensorHelper.FilterMessage(msg); - - if (ServerConfiguration.Instance.LogChatFiltering && filteredText != msg) - Logger.Info($"Censored profane word(s) from user comment sent by {username}: \"{msg}\" => \"{filteredText}\"", - LogArea.Filter); - - bool success = await this.database.PostComment(token.UserId, id, CommentType.Profile, filteredText); - if (success) - { - Logger.Success($"Posted comment from {username}: \"{filteredText}\" on user {id}", LogArea.Comments); - } - else - { - Logger.Error($"Failed to post comment from {username}: \"{filteredText}\" on user {id}", LogArea.Comments); - } - - return this.Redirect("~/user/" + id); - } - [HttpGet("heart")] public async Task HeartUser([FromRoute] int id) { diff --git a/ProjectLighthouse.Servers.Website/Pages/Errors/ErrorPage.cshtml b/ProjectLighthouse.Servers.Website/Pages/Errors/ErrorPage.cshtml new file mode 100644 index 000000000..61886b36a --- /dev/null +++ b/ProjectLighthouse.Servers.Website/Pages/Errors/ErrorPage.cshtml @@ -0,0 +1,45 @@ +@page "/error" +@using System.Globalization +@using LBPUnion.ProjectLighthouse.Configuration +@model LBPUnion.ProjectLighthouse.Servers.Website.Pages.Errors.ErrorPage + +@{ + Layout = null; +} + + + + + + An error has occurred + + + + + + + + +
+
+
+ Instance logo +

An error has occurred

+

The server encountered an error while processing your request

+

+ Try to refresh this page, if the problem persists,
please report + your problem and mention this error message +

+
+ Request ID: @Model.RequestId +
+ Timestamp: @DateTime.UtcNow.ToString(CultureInfo.InvariantCulture) UTC +
+
+
+
+ + \ No newline at end of file diff --git a/ProjectLighthouse.Servers.Website/Pages/Errors/ErrorPage.cshtml.cs b/ProjectLighthouse.Servers.Website/Pages/Errors/ErrorPage.cshtml.cs new file mode 100644 index 000000000..85cfe3d15 --- /dev/null +++ b/ProjectLighthouse.Servers.Website/Pages/Errors/ErrorPage.cshtml.cs @@ -0,0 +1,23 @@ +using Microsoft.AspNetCore.Diagnostics; +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc.RazorPages; + +namespace LBPUnion.ProjectLighthouse.Servers.Website.Pages.Errors; + +[ResponseCache(Duration = 0, Location = ResponseCacheLocation.None, NoStore = true)] +[IgnoreAntiforgeryToken] +public class ErrorPage : PageModel +{ + public string? RequestId { get; set; } + + public IActionResult OnGet() + { + this.RequestId = this.HttpContext.TraceIdentifier; + + IExceptionHandlerPathFeature? exceptionHandlerPathFeature = this.HttpContext.Features.Get(); + + if (exceptionHandlerPathFeature?.Error == null) return this.NotFound(); + + return this.Page(); + } +} \ No newline at end of file diff --git a/ProjectLighthouse.Servers.Website/Pages/Frames/CommentFrame.cshtml b/ProjectLighthouse.Servers.Website/Pages/Frames/CommentFrame.cshtml new file mode 100644 index 000000000..f59754ad4 --- /dev/null +++ b/ProjectLighthouse.Servers.Website/Pages/Frames/CommentFrame.cshtml @@ -0,0 +1,50 @@ +@page "/comments/{type}/{id:int}" +@using LBPUnion.ProjectLighthouse.Servers.Website.Extensions +@model LBPUnion.ProjectLighthouse.Servers.Website.Pages.Frames.CommentFrame + +@{ + Model.Title = "Comments"; + Layout = "PaginatedFrame"; + + string language = Model.GetLanguage(); + string timeZone = Model.GetTimeZone(); +} + +
+ @if (!Model.CanViewComments) + { + if (Model.Type == "user") + { + This user's privacy settings prevent you from viewing this page. + } + else + { + This level creator's privacy settings prevent you from viewing this page. + } + } + else if (Model.Comments.Count == 0 && Model.CommentsEnabled) + { +

There are no comments.

+ } + else if (!Model.CommentsEnabled) + { + Comments are disabled. + } + else + { + int count = Model.TotalItems; +

There @(count == 1 ? "is" : "are") @count comment@(count == 1 ? "" : "s").

+ @await Html.PartialAsync("Partials/CommentsPartial", (Model.User, Model.Comments, Model.CommentsEnabled), new ViewDataDictionary(ViewData.WithLang(language).WithTime(timeZone)) + { + { + "PageOwner", Model.PageOwner + }, + { + "BaseUrl", $"/{Model.Type}/{Model.Id}" + }, + { + "RedirectUrl", $"/comments/{Model.Type}/{Model.Id}?page=" + Model.CurrentPage + }, + }) + } +
\ No newline at end of file diff --git a/ProjectLighthouse.Servers.Website/Pages/Frames/CommentFrame.cshtml.cs b/ProjectLighthouse.Servers.Website/Pages/Frames/CommentFrame.cshtml.cs new file mode 100644 index 000000000..d59c65242 --- /dev/null +++ b/ProjectLighthouse.Servers.Website/Pages/Frames/CommentFrame.cshtml.cs @@ -0,0 +1,100 @@ +using LBPUnion.ProjectLighthouse.Configuration; +using LBPUnion.ProjectLighthouse.Database; +using LBPUnion.ProjectLighthouse.Extensions; +using LBPUnion.ProjectLighthouse.Types.Entities.Interaction; +using LBPUnion.ProjectLighthouse.Types.Entities.Level; +using LBPUnion.ProjectLighthouse.Types.Entities.Profile; +using LBPUnion.ProjectLighthouse.Types.Users; +using Microsoft.AspNetCore.Mvc; +using Microsoft.EntityFrameworkCore; + +namespace LBPUnion.ProjectLighthouse.Servers.Website.Pages.Frames; + +public class CommentFrame : PaginatedFrame +{ + public CommentFrame(DatabaseContext database) : base(database) + { + this.ItemsPerPage = 10; + } + + public Dictionary Comments = new(); + + public int Id { get; set; } + public string Type { get; set; } = ""; + + public bool CommentsEnabled { get; set; } + public bool CanViewComments { get; set; } + + public int PageOwner { get; set; } + + public async Task OnGet([FromQuery] int page, string type, int id) + { + this.Type = type; + this.Id = id; + this.CurrentPage = page; + CommentType? commentType = type switch + { + "slot" => CommentType.Level, + "user" => CommentType.Profile, + _ => null, + }; + switch (commentType) + { + case CommentType.Level: + { + SlotEntity? slot = await this.Database.Slots.Include(s => s.Creator) + .FirstOrDefaultAsync(s => s.SlotId == id); + if (slot == null || slot.Creator == null) return this.BadRequest(); + this.PageOwner = slot.CreatorId; + this.CanViewComments = slot.Creator.LevelVisibility.CanAccess(this.User != null, + slot.Creator == this.User || this.User != null && this.User.IsModerator); + this.CommentsEnabled = ServerConfiguration.Instance.UserGeneratedContentLimits.LevelCommentsEnabled && + slot.CommentsEnabled; + break; + } + case CommentType.Profile: + { + UserEntity? user = await this.Database.Users.FindAsync(id); + if (user == null) return this.BadRequest(); + this.PageOwner = user.UserId; + this.CanViewComments = user.ProfileVisibility.CanAccess(this.User != null, + user == this.User || this.User != null && this.User.IsModerator); + this.CommentsEnabled = ServerConfiguration.Instance.UserGeneratedContentLimits.ProfileCommentsEnabled && + user.CommentsEnabled; + break; + } + default: return this.BadRequest(); + } + + if (this.CommentsEnabled && this.CanViewComments) + { + List blockedUsers = await this.Database.GetBlockedUsers(this.User?.UserId); + + IQueryable commentQuery = this.Database.Comments.Include(p => p.Poster) + .Where(c => c.Poster.PermissionLevel != PermissionLevel.Banned) + .Where(c => c.TargetId == id && c.Type == commentType) + .Where(c => !blockedUsers.Contains(c.PosterUserId)); + + this.TotalItems = await commentQuery.CountAsync(); + + this.ClampPage(); + + int userId = this.User?.UserId ?? 0; + + this.Comments = await commentQuery.OrderByDescending(c => c.Timestamp) + .ApplyPagination(this.PageData) + .Select(c => new + { + Comment = c, + YourRating = this.Database.RatedComments.FirstOrDefault(r => r.CommentId == c.CommentId && r.UserId == userId), + }) + .ToDictionaryAsync(c => c.Comment, c => c.YourRating); + } + else + { + this.ClampPage(); + } + + return this.Page(); + } +} \ No newline at end of file diff --git a/ProjectLighthouse.Servers.Website/Pages/Frames/PaginatedFrame.cshtml b/ProjectLighthouse.Servers.Website/Pages/Frames/PaginatedFrame.cshtml new file mode 100644 index 000000000..f9ea6b56d --- /dev/null +++ b/ProjectLighthouse.Servers.Website/Pages/Frames/PaginatedFrame.cshtml @@ -0,0 +1,43 @@ +@model LBPUnion.ProjectLighthouse.Servers.Website.Pages.Frames.PaginatedFrame + +@{ + Layout = "Layouts/BaseFrame"; +} + +@RenderBody() + +@section Pagination +{ + + +} \ No newline at end of file diff --git a/ProjectLighthouse.Servers.Website/Pages/Frames/PaginatedFrame.cshtml.cs b/ProjectLighthouse.Servers.Website/Pages/Frames/PaginatedFrame.cshtml.cs new file mode 100644 index 000000000..0c0c553f9 --- /dev/null +++ b/ProjectLighthouse.Servers.Website/Pages/Frames/PaginatedFrame.cshtml.cs @@ -0,0 +1,30 @@ +using LBPUnion.ProjectLighthouse.Database; +using LBPUnion.ProjectLighthouse.Servers.Website.Pages.Layouts; +using LBPUnion.ProjectLighthouse.Types.Filter; + +namespace LBPUnion.ProjectLighthouse.Servers.Website.Pages.Frames; + +public abstract class PaginatedFrame : BaseFrame +{ + protected PaginatedFrame(DatabaseContext database) : base(database) + { } + + public int CurrentPage { get; set; } + public int TotalItems { get; set; } + public int ItemsPerPage { get; set; } + public int TotalPages => Math.Max(1, (int)Math.Ceiling(this.TotalItems / (float)this.ItemsPerPage)); + + public PaginationData PageData => + new() + { + MaxElements = this.ItemsPerPage, + PageSize = this.ItemsPerPage, + PageStart = (this.CurrentPage - 1) * this.ItemsPerPage + 1, + TotalElements = this.TotalItems, + }; + + /// + /// Only call after setting CurrentPage and TotalItems + /// + public void ClampPage() => this.CurrentPage = Math.Clamp(this.CurrentPage, 1, this.TotalPages); +} \ No newline at end of file diff --git a/ProjectLighthouse.Servers.Website/Pages/Frames/PhotoFrame.cshtml b/ProjectLighthouse.Servers.Website/Pages/Frames/PhotoFrame.cshtml new file mode 100644 index 000000000..a43108225 --- /dev/null +++ b/ProjectLighthouse.Servers.Website/Pages/Frames/PhotoFrame.cshtml @@ -0,0 +1,47 @@ +@page "/photos/{type}/{id:int}" +@using LBPUnion.ProjectLighthouse.Servers.Website.Extensions +@using LBPUnion.ProjectLighthouse.Types.Entities.Profile +@model LBPUnion.ProjectLighthouse.Servers.Website.Pages.Frames.PhotoFrame + +@{ + Layout = "PaginatedFrame"; + Model.Title = "Photos"; + string language = Model.GetLanguage(); + string timeZone = Model.GetTimeZone(); + bool isMobile = Model.IsMobile; +} + +
+ @if (!Model.CanViewPhotos) + { + if (Model.Type == "user") + { + This user's privacy settings prevent you from viewing this page. + } + else + { + This level creator's privacy settings prevent you from viewing this page. + } + } + else if (Model.Photos.Count > 0) + { +
+ @foreach (PhotoEntity photo in Model.Photos) + { + string width = isMobile ? "sixteen" : "eight"; + bool canDelete = Model.User != null && (Model.User.IsModerator || Model.User.UserId == photo.CreatorId); +
+ @await photo.ToHtml(Html, ViewData, language, timeZone, canDelete) +
+ } +
+ @if (isMobile) + { +
+ } + } + else + { +

There are no photos

+ } +
\ No newline at end of file diff --git a/ProjectLighthouse.Servers.Website/Pages/Frames/PhotoFrame.cshtml.cs b/ProjectLighthouse.Servers.Website/Pages/Frames/PhotoFrame.cshtml.cs new file mode 100644 index 000000000..b95a31a9f --- /dev/null +++ b/ProjectLighthouse.Servers.Website/Pages/Frames/PhotoFrame.cshtml.cs @@ -0,0 +1,67 @@ +using LBPUnion.ProjectLighthouse.Database; +using LBPUnion.ProjectLighthouse.Extensions; +using LBPUnion.ProjectLighthouse.Types.Entities.Level; +using LBPUnion.ProjectLighthouse.Types.Entities.Profile; +using LBPUnion.ProjectLighthouse.Types.Users; +using Microsoft.AspNetCore.Mvc; +using Microsoft.EntityFrameworkCore; + +namespace LBPUnion.ProjectLighthouse.Servers.Website.Pages.Frames; + +public class PhotoFrame : PaginatedFrame +{ + public List Photos { get; set; } = new(); + + public bool CanViewPhotos { get; set; } + public string Type { get; set; } = ""; + + public PhotoFrame(DatabaseContext database) : base(database) + { + this.ItemsPerPage = 10; + } + + public async Task OnGet([FromQuery] int page, string type, int id) + { + this.CurrentPage = page; + this.Type = type; + if (type != "user" && type != "slot") return this.BadRequest(); + + IQueryable photoQuery = this.Database.Photos.Include(p => p.Slot) + .Include(p => p.PhotoSubjects) + .ThenInclude(ps => ps.User); + + switch (type) + { + case "user": + UserEntity? user = await this.Database.Users.FindAsync(id); + if (user == null) return this.NotFound(); + this.CanViewPhotos = user.ProfileVisibility.CanAccess(this.User != null, + this.User == user || this.User != null && this.User.IsModerator); + photoQuery = photoQuery.Where(p => p.CreatorId == id); + break; + case "slot": + SlotEntity? slot = await this.Database.Slots.Include(s => s.Creator) + .Where(s => s.SlotId == id) + .FirstOrDefaultAsync(); + if (slot == null || slot.Creator == null) return this.NotFound(); + this.CanViewPhotos = slot.Creator.LevelVisibility.CanAccess(this.User != null, + this.User == slot.Creator || this.User != null && this.User.IsModerator); + photoQuery = photoQuery.Where(p => p.SlotId == id); + break; + } + + if (!this.CanViewPhotos) + { + this.ClampPage(); + return this.Page(); + } + + this.TotalItems = await photoQuery.CountAsync(); + + this.ClampPage(); + + this.Photos = await photoQuery.OrderByDescending(p => p.Timestamp).ApplyPagination(this.PageData).ToListAsync(); + + return this.Page(); + } +} \ No newline at end of file diff --git a/ProjectLighthouse.Servers.Website/Pages/Frames/ReviewFrame.cshtml b/ProjectLighthouse.Servers.Website/Pages/Frames/ReviewFrame.cshtml new file mode 100644 index 000000000..58c20dfc9 --- /dev/null +++ b/ProjectLighthouse.Servers.Website/Pages/Frames/ReviewFrame.cshtml @@ -0,0 +1,39 @@ +@page "/reviews/{slotId:int}" +@using LBPUnion.ProjectLighthouse.Extensions +@model LBPUnion.ProjectLighthouse.Servers.Website.Pages.Frames.ReviewFrame + +@{ + Layout = "PaginatedFrame"; +} + +
+ @if (!Model.CanViewReviews) + { + This level creator's privacy settings prevent you from viewing this page. + } + else if (Model.Reviews.Count == 0 && Model.ReviewsEnabled) + { +

There are no reviews.

+ } + else if (!Model.ReviewsEnabled) + { + + Reviews are disabled on this level. + + } + else + { + int count = Model.TotalItems; +

There @(count == 1 ? "is" : "are") @count review@(count == 1 ? "" : "s").

+
+ @await Html.PartialAsync("Partials/ReviewPartial", Model.Reviews, new ViewDataDictionary(ViewData) + { + { + "isMobile", Request.IsMobile() + }, + { + "CanDelete", Model.User?.IsModerator ?? false + }, + }) + } +
\ No newline at end of file diff --git a/ProjectLighthouse.Servers.Website/Pages/Frames/ReviewFrame.cshtml.cs b/ProjectLighthouse.Servers.Website/Pages/Frames/ReviewFrame.cshtml.cs new file mode 100644 index 000000000..7cbf0f7b1 --- /dev/null +++ b/ProjectLighthouse.Servers.Website/Pages/Frames/ReviewFrame.cshtml.cs @@ -0,0 +1,60 @@ +using LBPUnion.ProjectLighthouse.Configuration; +using LBPUnion.ProjectLighthouse.Database; +using LBPUnion.ProjectLighthouse.Extensions; +using LBPUnion.ProjectLighthouse.Types.Entities.Level; +using LBPUnion.ProjectLighthouse.Types.Users; +using Microsoft.AspNetCore.Mvc; +using Microsoft.EntityFrameworkCore; + +namespace LBPUnion.ProjectLighthouse.Servers.Website.Pages.Frames; + +public class ReviewFrame : PaginatedFrame +{ + public List Reviews = new(); + + public bool CanViewReviews { get; set; } + public bool ReviewsEnabled = ServerConfiguration.Instance.UserGeneratedContentLimits.LevelReviewsEnabled; + + public ReviewFrame(DatabaseContext database) : base(database) + { + this.ItemsPerPage = 10; + } + + public async Task OnGet([FromQuery] int page, int slotId) + { + this.CurrentPage = page; + + SlotEntity? slot = await this.Database.Slots.Include(s => s.Creator) + .Where(s => s.SlotId == slotId) + .FirstOrDefaultAsync(); + if (slot == null || slot.Creator == null) return this.BadRequest(); + + this.CanViewReviews = slot.Creator.LevelVisibility.CanAccess(this.User != null, + this.User == slot.Creator || this.User != null && this.User.IsModerator); + + if (!this.ReviewsEnabled || !this.CanViewReviews) + { + this.ClampPage(); + return this.Page(); + } + + List blockedUsers = await this.Database.GetBlockedUsers(this.User?.UserId); + + IQueryable reviewQuery = this.Database.Reviews.Where(r => r.SlotId == slotId) + .Where(r => !blockedUsers.Contains(r.ReviewerId)) + .Include(r => r.Reviewer) + .Where(r => r.Reviewer.PermissionLevel != PermissionLevel.Banned); + + this.TotalItems = await reviewQuery.CountAsync(); + + this.ClampPage(); + + this.Reviews = await reviewQuery.OrderByDescending(r => r.ThumbsUp - r.ThumbsDown) + .ThenByDescending(r => r.Timestamp) + .ApplyPagination(this.PageData) + .ToListAsync(); + + return this.Page(); + } + +} \ No newline at end of file diff --git a/ProjectLighthouse.Servers.Website/Pages/Frames/ScoreFrame.cshtml b/ProjectLighthouse.Servers.Website/Pages/Frames/ScoreFrame.cshtml new file mode 100644 index 000000000..259bb5e92 --- /dev/null +++ b/ProjectLighthouse.Servers.Website/Pages/Frames/ScoreFrame.cshtml @@ -0,0 +1,27 @@ +@page "/scores/{slotId:int}/{scoreType:int?}" +@using LBPUnion.ProjectLighthouse.Servers.Website.Extensions +@model LBPUnion.ProjectLighthouse.Servers.Website.Pages.Frames.ScoreFrame + +@{ + Layout = "PaginatedFrame"; + string language = Model.GetLanguage(); + string timeZone = Model.GetTimeZone(); +} + +
+ @if (!Model.CanViewScores) + { + This level creator's privacy settings prevent you from viewing this page. + } + else if (Model.TotalItems == 0) + { +

There are no scores.

+ } + else + { + int count = Model.TotalItems; +

There @(count == 1 ? "is" : "are") @count score@(count == 1 ? "" : "s").

+
+ @await Html.PartialAsync("Partials/LeaderboardPartial", (Model.Scores, Model.ScoreType), ViewData.WithLang(language).WithTime(timeZone).CanDelete(Model.User?.IsModerator ?? false)) + } +
\ No newline at end of file diff --git a/ProjectLighthouse.Servers.Website/Pages/Frames/ScoreFrame.cshtml.cs b/ProjectLighthouse.Servers.Website/Pages/Frames/ScoreFrame.cshtml.cs new file mode 100644 index 000000000..e2f18c11a --- /dev/null +++ b/ProjectLighthouse.Servers.Website/Pages/Frames/ScoreFrame.cshtml.cs @@ -0,0 +1,75 @@ +using LBPUnion.ProjectLighthouse.Database; +using LBPUnion.ProjectLighthouse.Extensions; +using LBPUnion.ProjectLighthouse.Types.Entities.Level; +using LBPUnion.ProjectLighthouse.Types.Users; +using Microsoft.AspNetCore.Mvc; +using Microsoft.EntityFrameworkCore; + +namespace LBPUnion.ProjectLighthouse.Servers.Website.Pages.Frames; + +public class ScoreFrame : PaginatedFrame +{ + public List<(int Rank, ScoreEntity Score)> Scores = new(); + + public int ScoreType { get; set; } + + public bool CanViewScores { get; set; } + + public ScoreFrame(DatabaseContext database) : base(database) + { + this.ItemsPerPage = 10; + } + + public async Task OnGet([FromQuery] int page, int slotId, int? scoreType) + { + this.CurrentPage = page; + + SlotEntity? slot = await this.Database.Slots.Include(s => s.Creator) + .Where(s => s.SlotId == slotId) + .FirstOrDefaultAsync(); + if (slot == null || slot.Creator == null) return this.BadRequest(); + + this.CanViewScores = slot.Creator.LevelVisibility.CanAccess(this.User != null, + this.User == slot.Creator || this.User != null && this.User.IsModerator); + + if (!this.CanViewScores) + { + this.ClampPage(); + return this.Page(); + } + + scoreType ??= slot.LevelType switch + { + "versus" => 7, + _ => 1, + }; + + Func isValidFunc = slot.LevelType switch + { + "versus" => type => type == 7, + _ => type => type is >= 1 and <= 4, + }; + + if (!isValidFunc(scoreType)) return this.BadRequest(); + + IQueryable scoreQuery = this.Database.Scores.Where(s => s.SlotId == slotId) + .Where(s => s.Type == scoreType); + + this.TotalItems = await scoreQuery.CountAsync(); + + this.ClampPage(); + + this.Scores = (await scoreQuery.OrderByDescending(s => s.Points) + .ThenBy(s => s.ScoreId) + .Select(s => new + { + Rank = scoreQuery.Count(s2 => s2.Points > s.Points) + 1, + Score = s, + }) + .ApplyPagination(this.PageData) + .ToListAsync()).Select(s => (s.Rank, s.Score)) + .ToList(); + + return this.Page(); + } +} \ No newline at end of file diff --git a/ProjectLighthouse.Servers.Website/Pages/Frames/SlotsFrame.cshtml b/ProjectLighthouse.Servers.Website/Pages/Frames/SlotsFrame.cshtml new file mode 100644 index 000000000..36983e9ea --- /dev/null +++ b/ProjectLighthouse.Servers.Website/Pages/Frames/SlotsFrame.cshtml @@ -0,0 +1,33 @@ +@page "/slots/{route}/{id:int}" +@using LBPUnion.ProjectLighthouse.Servers.Website.Extensions +@using LBPUnion.ProjectLighthouse.Types.Entities.Level +@model LBPUnion.ProjectLighthouse.Servers.Website.Pages.Frames.SlotsFrame + +@{ + Layout = "PaginatedFrame"; + Model.Title = "Photos"; + string language = Model.GetLanguage(); + string timeZone = Model.GetTimeZone(); + bool isMobile = Model.IsMobile; +} + +
+ @if (!Model.CanViewSlots) + { +

This user's privacy settings prevent you from viewing this page.

+ } + else if (Model.Slots.Count == 0) + { +

@Model.SlotsEmptyText

+ } + else + { +

@Model.SlotsPresentText

+ foreach (SlotEntity slot in Model.Slots) + { +
+ @await slot.ToHtml(Html, ViewData, Model.User, $"~/slots/{Model.Type}/{Model.Id}", language, timeZone, isMobile, true) +
+ } + } +
\ No newline at end of file diff --git a/ProjectLighthouse.Servers.Website/Pages/Frames/SlotsFrame.cshtml.cs b/ProjectLighthouse.Servers.Website/Pages/Frames/SlotsFrame.cshtml.cs new file mode 100644 index 000000000..b7026fcdb --- /dev/null +++ b/ProjectLighthouse.Servers.Website/Pages/Frames/SlotsFrame.cshtml.cs @@ -0,0 +1,96 @@ +using LBPUnion.ProjectLighthouse.Database; +using LBPUnion.ProjectLighthouse.Extensions; +using LBPUnion.ProjectLighthouse.Types.Entities.Level; +using LBPUnion.ProjectLighthouse.Types.Entities.Profile; +using LBPUnion.ProjectLighthouse.Types.Users; +using Microsoft.AspNetCore.Mvc; +using Microsoft.EntityFrameworkCore; + +namespace LBPUnion.ProjectLighthouse.Servers.Website.Pages.Frames; + +public class SlotsFrame : PaginatedFrame +{ + public List Slots = new(); + + public string Type { get; set; } = ""; + public int Id { get; set; } + public string Color { get; set; } = ""; + public string SlotsPresentText { get; set; } = ""; + public string SlotsEmptyText { get; set; } = ""; + + public bool CanViewSlots { get; set; } + + public SlotsFrame(DatabaseContext database) : base(database) + { + this.ItemsPerPage = 10; + } + + public async Task OnGet([FromQuery] int page, string route, int id) + { + this.Type = route; + this.Id = id; + this.CurrentPage = page; + + UserEntity? user = await this.Database.Users.FindAsync(id); + if (user == null) return this.NotFound(); + + this.CanViewSlots = user.ProfileVisibility.CanAccess(this.User != null, + this.User == user || this.User != null && this.User.IsModerator); + + if (!this.CanViewSlots) + { + this.Color = "green"; + this.ClampPage(); + return this.Page(); + } + + IQueryable slotsQuery = this.Database.Slots.AsQueryable(); + + switch (route) + { + case "by": + slotsQuery = this.Database.Slots.Include(p => p.Creator) + .Where(p => p.CreatorId == id) + .OrderByDescending(s => s.LastUpdated); + this.Color = "green"; + this.SlotsEmptyText = "This user hasn't published any levels"; + this.SlotsPresentText = "This user has published {0} level{1}"; + break; + case "hearted": + if (this.User != user) return this.BadRequest(); + slotsQuery = this.Database.HeartedLevels.Where(h => h.UserId == id) + .OrderByDescending(h => h.HeartedLevelId) + .Include(h => h.Slot) + .ThenInclude(s => s.Creator) + .Select(h => h.Slot); + this.Color = "pink"; + this.SlotsEmptyText = "You haven't hearted any levels"; + this.SlotsPresentText = "You have hearted {0} level{1}"; + break; + case "queued": + if (this.User != user) return this.BadRequest(); + slotsQuery = this.Database.QueuedLevels.Where(q => q.UserId == id) + .OrderByDescending(q => q.QueuedLevelId) + .Include(q => q.Slot) + .ThenInclude(s => s.Creator) + .Select(q => q.Slot); + this.Color = "yellow"; + this.SlotsEmptyText = "You haven't queued any levels"; + this.SlotsPresentText = "There are {0} level{1} in your queue"; + break; + } + + this.TotalItems = await slotsQuery.CountAsync(); + + this.ClampPage(); + + if (this.TotalItems != 0) + { + this.SlotsPresentText = string.Format(this.SlotsPresentText, this.TotalItems, this.TotalItems == 1 ? "" : "s"); + } + + this.Slots = await slotsQuery.ApplyPagination(this.PageData).ToListAsync(); + + return this.Page(); + } +} \ No newline at end of file diff --git a/ProjectLighthouse.Servers.Website/Pages/LandingPage.cshtml b/ProjectLighthouse.Servers.Website/Pages/LandingPage.cshtml index 903dbf0cb..fb24761eb 100644 --- a/ProjectLighthouse.Servers.Website/Pages/LandingPage.cshtml +++ b/ProjectLighthouse.Servers.Website/Pages/LandingPage.cshtml @@ -62,7 +62,7 @@

-
+

diff --git a/ProjectLighthouse.Servers.Website/Pages/Layouts/BaseFrame.cshtml b/ProjectLighthouse.Servers.Website/Pages/Layouts/BaseFrame.cshtml new file mode 100644 index 000000000..6d460eca1 --- /dev/null +++ b/ProjectLighthouse.Servers.Website/Pages/Layouts/BaseFrame.cshtml @@ -0,0 +1,39 @@ +@using LBPUnion.ProjectLighthouse.Extensions +@model LBPUnion.ProjectLighthouse.Servers.Website.Pages.Layouts.BaseFrame + + + +@{ + Model.IsMobile = Model.Request.IsMobile(); +} + + + + @Model.Title + + + + + +
+ @RenderBody() + + @await RenderSectionAsync("Pagination", false) +
+ + \ No newline at end of file diff --git a/ProjectLighthouse.Servers.Website/Pages/Layouts/BaseFrame.cshtml.cs b/ProjectLighthouse.Servers.Website/Pages/Layouts/BaseFrame.cshtml.cs new file mode 100644 index 000000000..e0bd18fa7 --- /dev/null +++ b/ProjectLighthouse.Servers.Website/Pages/Layouts/BaseFrame.cshtml.cs @@ -0,0 +1,9 @@ +using LBPUnion.ProjectLighthouse.Database; + +namespace LBPUnion.ProjectLighthouse.Servers.Website.Pages.Layouts; + +public class BaseFrame : BaseLayout +{ + public BaseFrame(DatabaseContext database) : base(database) + { } +} \ No newline at end of file diff --git a/ProjectLighthouse.Servers.Website/Pages/Layouts/BaseLayout.cshtml b/ProjectLighthouse.Servers.Website/Pages/Layouts/BaseLayout.cshtml index 89ff3a8bd..5798c3bd5 100644 --- a/ProjectLighthouse.Servers.Website/Pages/Layouts/BaseLayout.cshtml +++ b/ProjectLighthouse.Servers.Website/Pages/Layouts/BaseLayout.cshtml @@ -2,6 +2,7 @@ @using LBPUnion.ProjectLighthouse.Extensions @using LBPUnion.ProjectLighthouse.Helpers @using LBPUnion.ProjectLighthouse.Localization.StringLists +@using LBPUnion.ProjectLighthouse.Servers.Website.Pages.Errors @using LBPUnion.ProjectLighthouse.Servers.Website.Types @model LBPUnion.ProjectLighthouse.Servers.Website.Pages.Layouts.BaseLayout @@ -63,6 +64,19 @@ } + + @* If non-error page is loaded in iFrame, redirect top level window *@ + @if (Model.GetType() != typeof(NotFoundPage) && Model.GetType() != typeof(ErrorPage)) + { + + } @* Google Analytics *@ @if (ServerConfiguration.Instance.GoogleAnalytics.AnalyticsEnabled) diff --git a/ProjectLighthouse.Servers.Website/Pages/Partials/CommentsPartial.cshtml b/ProjectLighthouse.Servers.Website/Pages/Partials/CommentsPartial.cshtml index aa986c03c..56c3a3983 100644 --- a/ProjectLighthouse.Servers.Website/Pages/Partials/CommentsPartial.cshtml +++ b/ProjectLighthouse.Servers.Website/Pages/Partials/CommentsPartial.cshtml @@ -4,114 +4,99 @@ @using LBPUnion.ProjectLighthouse.Servers.Website.Extensions @using LBPUnion.ProjectLighthouse.Types.Entities.Interaction @using LBPUnion.ProjectLighthouse.Types.Entities.Profile +@model (UserEntity? User, Dictionary Comments, bool CommentsEnabled) @inject DatabaseContext Database - @{ string language = (string?)ViewData["Language"] ?? LocalizationManager.DefaultLang; string timeZone = (string?)ViewData["TimeZone"] ?? TimeZoneInfo.Local.Id; TimeZoneInfo timeZoneInfo = TimeZoneInfo.FindSystemTimeZoneById(timeZone); int pageOwnerId = (int?)ViewData["PageOwner"] ?? 0; + string baseUrl = (string?)ViewData["BaseUrl"] ?? Url.RouteUrl(ViewContext.RouteData.Values) ?? "~/"; + string redirectUrl = (string?)ViewData["RedirectUrl"] ?? ""; } -
- @if (Model.Comments.Count == 0 && Model.CommentsEnabled) - { - There are no comments. - } - else if (!Model.CommentsEnabled) - { - Comments are disabled. - } - else - { - int count = Model.Comments.Count; -

There @(count == 1 ? "is" : "are") @count comment@(count == 1 ? "" : "s").

- } - @if (Model.CommentsEnabled && Model.User != null) +@if (Model.CommentsEnabled && Model.User != null) +{ +
+ +
+
+ +
+ +
+ @if (Model.Comments.Count > 0) {
-
-
- -
- -
- @if (Model.Comments.Count > 0) - { -
- } } - - @{ - int i = 0; - foreach (KeyValuePair commentAndReaction in Model.Comments) - { - CommentEntity comment = commentAndReaction.Key; - int yourThumb = commentAndReaction.Value?.Rating ?? 0; - DateTimeOffset timestamp = DateTimeOffset.FromUnixTimeSeconds(comment.Timestamp / 1000).ToLocalTime(); +} - string decodedMessage = HttpUtility.HtmlDecode(comment.GetCommentMessage(Database)); - - string? url = Url.RouteUrl(ViewContext.RouteData.Values); - if (url == null) continue; +@{ + int i = 0; + foreach ((CommentEntity? comment, RatedCommentEntity? value) in Model.Comments) + { + int yourThumb = value?.Rating ?? 0; + DateTimeOffset timestamp = DateTimeOffset.FromUnixTimeSeconds(comment.Timestamp / 1000).ToLocalTime(); + + string decodedMessage = HttpUtility.HtmlDecode(comment.GetCommentMessage(Database)); - int rating = comment.ThumbsUp - comment.ThumbsDown; + int rating = comment.ThumbsUp - comment.ThumbsDown; -
- @{ - string style = ""; - if (Model.User?.UserId == comment.PosterUserId) - { - style = "pointer-events: none"; - } +
+ @{ + string style = ""; + if (Model.User?.UserId == comment.PosterUserId) + { + style = "pointer-events: none"; } -
- - - - @(rating) - - - -
+ } -
- @await comment.Poster.ToLink(Html, ViewData, language): - @if (comment.Deleted) - { - - @decodedMessage - - } - else - { +
+ + + + @(rating) + + + +
+ +
+ @await comment.Poster.ToLink(Html, ViewData, language): + @if (comment.Deleted) + { + @decodedMessage - } - @if (((Model.User?.IsModerator ?? false) || Model.User?.UserId == comment.PosterUserId || Model.User?.UserId == pageOwnerId) && !comment.Deleted) - { - - } -

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

- @if (i != Model.Comments.Count - 1) - { -
- } -
+ + } + else + { + @decodedMessage + } + @if (((Model.User?.IsModerator ?? false) || Model.User?.UserId == comment.PosterUserId || Model.User?.UserId == pageOwnerId) && !comment.Deleted) + { + + } +

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

+ @if (i != Model.Comments.Count - 1) + { +
+ }
- i++; - } +
+ i++; } - -
\ No newline at end of file + \ No newline at end of file diff --git a/ProjectLighthouse.Servers.Website/Pages/Partials/FramePartial.cshtml b/ProjectLighthouse.Servers.Website/Pages/Partials/FramePartial.cshtml new file mode 100644 index 000000000..dc984a676 --- /dev/null +++ b/ProjectLighthouse.Servers.Website/Pages/Partials/FramePartial.cshtml @@ -0,0 +1,8 @@ +@model (string Url, string Title) + + \ No newline at end of file diff --git a/ProjectLighthouse.Servers.Website/Pages/Partials/LeaderboardPartial.cshtml b/ProjectLighthouse.Servers.Website/Pages/Partials/LeaderboardPartial.cshtml index 797fac6d2..99df063c6 100644 --- a/ProjectLighthouse.Servers.Website/Pages/Partials/LeaderboardPartial.cshtml +++ b/ProjectLighthouse.Servers.Website/Pages/Partials/LeaderboardPartial.cshtml @@ -4,44 +4,35 @@ @using LBPUnion.ProjectLighthouse.Types.Entities.Level @using LBPUnion.ProjectLighthouse.Types.Entities.Profile @using Microsoft.EntityFrameworkCore +@model (List<(int Rank, ScoreEntity Score)> Scores, int scoreType) +@inject DatabaseContext Database @{ string language = (string?)ViewData["Language"] ?? LocalizationManager.DefaultLang; string timeZone = (string?)ViewData["TimeZone"] ?? TimeZoneInfo.Local.Id; bool canDelete = (bool?)ViewData["CanDelete"] ?? false; -} -
- @if (Model.Scores.Count == 0) +} + +
+ @for (int i = 0; i < Model.Scores.Count; i++) { -

There are no scores.

- } - else - { - int count = Model.Scores.Count; -

There @(count == 1 ? "is" : "are") @count score@(count == 1 ? "" : "s").

-
- } -
- @for(int i = 0; i < Model.Scores.Count; i++) - { - ScoreEntity score = Model.Scores[i]; - string[] playerIds = score.PlayerIds; - DatabaseContext database = Model.Database; + (int Rank, ScoreEntity Score) scoreAndRank = Model.Scores[i]; + string[] playerIds = scoreAndRank.Score.PlayerIds;
- @if(canDelete) + @if (canDelete) { - - } - @(i+1): - @score.Points points + + } + @(scoreAndRank.Rank): + @scoreAndRank.Score.Points points
@for (int j = 0; j < playerIds.Length; j++) { - UserEntity? user = await database.Users.FirstOrDefaultAsync(u => u.Username == playerIds[j]); + UserEntity? user = await Database.Users.FirstOrDefaultAsync(u => u.Username == playerIds[j]);
@@ -64,13 +55,12 @@
} } -
- -
\ No newline at end of file + \ No newline at end of file diff --git a/ProjectLighthouse.Servers.Website/Pages/Partials/Links/UserLinkPartial.cshtml b/ProjectLighthouse.Servers.Website/Pages/Partials/Links/UserLinkPartial.cshtml index 4d889b336..33cc138af 100644 --- a/ProjectLighthouse.Servers.Website/Pages/Partials/Links/UserLinkPartial.cshtml +++ b/ProjectLighthouse.Servers.Website/Pages/Partials/Links/UserLinkPartial.cshtml @@ -12,7 +12,7 @@ string userStatus = includeStatus ? Model.GetStatus(Database).ToTranslatedString(language, timeZone) : ""; } - + @if (Model.IsModerator) @@ -25,5 +25,4 @@ { @Model.Username } - \ No newline at end of file diff --git a/ProjectLighthouse.Servers.Website/Pages/Partials/PhotoPartial.cshtml b/ProjectLighthouse.Servers.Website/Pages/Partials/PhotoPartial.cshtml index 70b98e64a..8691f758a 100644 --- a/ProjectLighthouse.Servers.Website/Pages/Partials/PhotoPartial.cshtml +++ b/ProjectLighthouse.Servers.Website/Pages/Partials/PhotoPartial.cshtml @@ -28,7 +28,7 @@ } } - @(Model.Slot.IsAdventurePlanet ? "on an adventure in" : "in level") - @HttpUtility.HtmlDecode(Model.Slot.Name) + + @HttpUtility.HtmlDecode(Model.Slot.Name) + break; case SlotType.Developer: @@ -93,11 +95,14 @@ GamePhotoSubject[] subjects = Model.PhotoSubjects.Select(GamePhotoSubject.CreateFromEntity).ToArray(); foreach (GamePhotoSubject subject in subjects) { + subject.Bounds ??= ""; subject.Username = Model.PhotoSubjects.Where(ps => ps.UserId == subject.UserId).Select(ps => ps.User.Username).First(); } } - + +} \ No newline at end of file diff --git a/ProjectLighthouse.Servers.Website/Pages/Partials/ReviewPartial.cshtml b/ProjectLighthouse.Servers.Website/Pages/Partials/ReviewPartial.cshtml index 610342658..10dfe61fc 100644 --- a/ProjectLighthouse.Servers.Website/Pages/Partials/ReviewPartial.cshtml +++ b/ProjectLighthouse.Servers.Website/Pages/Partials/ReviewPartial.cshtml @@ -4,112 +4,94 @@ @using LBPUnion.ProjectLighthouse.Helpers @using LBPUnion.ProjectLighthouse.Types.Entities.Level @using LBPUnion.ProjectLighthouse.Types.Serialization +@model List @{ bool isMobile = (bool?)ViewData["IsMobile"] ?? false; bool canDelete = (bool?)ViewData["CanDelete"] ?? false; } -
-
- @if (Model.Reviews.Count == 0 && Model.ReviewsEnabled) - { -

There are no reviews.

- } - else if (!Model.ReviewsEnabled) - { - - Reviews are disabled on this level. - - } - else - { - int count = Model.Reviews.Count; -

There @(count == 1 ? "is" : "are") @count review@(count == 1 ? "" : "s").

-
- } - - @for(int i = 0; i < Model.Reviews.Count; i++) - { - ReviewEntity review = Model.Reviews[i]; - string faceHash = (review.Thumb switch { - -1 => review.Reviewer?.BooHash, - 0 => review.Reviewer?.MehHash, - 1 => review.Reviewer?.YayHash, +@for (int i = 0; i < Model.Count; i++) +{ + ReviewEntity review = Model[i]; + string faceHash = review.Thumb switch { + -1 => review.Reviewer?.BooHash, + 0 => review.Reviewer?.MehHash, + 1 => review.Reviewer?.YayHash, - _ => throw new ArgumentOutOfRangeException(), - }) ?? ""; + _ => throw new ArgumentOutOfRangeException(), + } ?? ""; - if (string.IsNullOrWhiteSpace(faceHash) || !FileHelper.ResourceExists(faceHash)) - { - faceHash = ServerConfiguration.Instance.WebsiteConfiguration.MissingIconHash; - } + if (string.IsNullOrWhiteSpace(faceHash) || !FileHelper.ResourceExists(faceHash)) + { + faceHash = ServerConfiguration.Instance.WebsiteConfiguration.MissingIconHash; + } - string faceAlt = review.Thumb switch { - -1 => "Boo!", - 0 => "Meh.", - 1 => "Yay!", + string faceAlt = review.Thumb switch { + -1 => "Boo!", + 0 => "Meh.", + 1 => "Yay!", - _ => throw new ArgumentOutOfRangeException(), - }; + _ => throw new ArgumentOutOfRangeException(), + }; - int size = isMobile ? 50 : 100; + int size = isMobile ? 50 : 100; -
-
- @faceAlt -
-
- -

@review.Reviewer?.Username

-
- @if (review.Deleted) - { - if (review.DeletedBy == DeletedBy.LevelAuthor) - { -

- This review has been deleted by the level author. -

- } - else - { -

- This review has been deleted by a moderator. -

- } - } - else +
+
+ @faceAlt +
+
+ +

@review.Reviewer?.Username

+
+ @if (review.Deleted) + { + if (review.DeletedBy == DeletedBy.LevelAuthor) + { +

+ This review has been deleted by the level author. +

+ } + else + { +

+ This review has been deleted by a moderator. +

+ } + } + else + { + @if (review.Labels.Length > 1) + { +
+ @foreach (string reviewLabel in review.Labels) { - @if (review.Labels.Length > 1) - { -
- @foreach (string reviewLabel in review.Labels) - { -
@LabelHelper.TranslateTag(reviewLabel)
- } -
- } - @if (string.IsNullOrWhiteSpace(review.Text)) - { -

- This review contains no text. -

- } - else - { - { -

@HttpUtility.HtmlDecode(review.Text)

- } - } +
@LabelHelper.TranslateTag(reviewLabel)
}
- @if (canDelete && !review.Deleted) + } + @if (string.IsNullOrWhiteSpace(review.Text)) + { +

+ This review contains no text. +

+ } + else + { { -
- - -
- } -
- @if (i != Model.Reviews.Count - 1) - { -
- } - } -
- @if (isMobile) - { -
+
} -
\ No newline at end of file +
+ @if (i != Model.Count - 1) + { +
+ } +} +@if (isMobile) +{ +
+} \ No newline at end of file diff --git a/ProjectLighthouse.Servers.Website/Pages/Partials/SectionScriptPartial.cshtml b/ProjectLighthouse.Servers.Website/Pages/Partials/SectionScriptPartial.cshtml new file mode 100644 index 000000000..53e2287bc --- /dev/null +++ b/ProjectLighthouse.Servers.Website/Pages/Partials/SectionScriptPartial.cshtml @@ -0,0 +1,93 @@ + \ No newline at end of file diff --git a/ProjectLighthouse.Servers.Website/Pages/Partials/SlotCardPartial.cshtml b/ProjectLighthouse.Servers.Website/Pages/Partials/SlotCardPartial.cshtml index 43ac34237..e74e1f535 100644 --- a/ProjectLighthouse.Servers.Website/Pages/Partials/SlotCardPartial.cshtml +++ b/ProjectLighthouse.Servers.Website/Pages/Partials/SlotCardPartial.cshtml @@ -53,7 +53,7 @@ @if (showLink) {

- @slotName + @slotName

} else @@ -68,7 +68,7 @@ @if (showLink) {

- @slotName + @slotName

} else @@ -114,12 +114,12 @@
@if (user != null && !mini && (user.IsModerator || Model.CreatorId == user.UserId)) { - + } - @if (user != null && !mini && (user.UserId != Model.CreatorId)) + @if (user != null && !mini && user.UserId != Model.CreatorId) { diff --git a/ProjectLighthouse.Servers.Website/Pages/SlotPage.cshtml b/ProjectLighthouse.Servers.Website/Pages/SlotPage.cshtml index d6e6ca2d5..97cdb3fa3 100644 --- a/ProjectLighthouse.Servers.Website/Pages/SlotPage.cshtml +++ b/ProjectLighthouse.Servers.Website/Pages/SlotPage.cshtml @@ -5,7 +5,6 @@ @using LBPUnion.ProjectLighthouse.Helpers @using LBPUnion.ProjectLighthouse.Localization.StringLists @using LBPUnion.ProjectLighthouse.Servers.Website.Extensions -@using LBPUnion.ProjectLighthouse.Types.Entities.Profile @using LBPUnion.ProjectLighthouse.Types.Moderation.Cases @using LBPUnion.ProjectLighthouse.Types.Users @model LBPUnion.ProjectLighthouse.Servers.Website.Pages.SlotPage @@ -56,9 +55,8 @@ } @await Model.Slot.ToHtml(Html, ViewData, Model.User, $"~/slot/{Model.Slot?.SlotId}", language, timeZone, isMobile) -
-@if (!Model.CanViewSlot) +@if (!Model.CanViewLevel) {

@@ -68,7 +66,7 @@ } else { -

+

Description

@@ -138,64 +136,38 @@ else string divLength = isMobile ? "sixteen" : "thirteen"; }
-
- @await Html.PartialAsync("Partials/CommentsPartial", new ViewDataDictionary(ViewData.WithLang(language).WithTime(timeZone)) - { - { - "PageOwner", Model.Slot?.CreatorId - }, - }) -
-
-
- @if (Model.Photos.Count != 0) - { -
- @foreach (PhotoEntity photo in Model.Photos) - { - string width = isMobile ? "sixteen" : "eight"; - bool canDelete = Model.User != null && (Model.User.IsModerator || Model.User.UserId == photo.CreatorId); -
- @await photo.ToHtml(Html, ViewData, language, timeZone, canDelete) -
- } -
- @if (isMobile) +
+ @for (int i = 0; i < 3; i++) + { +
+ @for (int j = 0; j < 5; j++) { -
+
} - } - else - { -

This level has no photos yet.

- } - -
+
+ }
-
- @await Html.PartialAsync("Partials/ReviewPartial", new ViewDataDictionary(ViewData) - { - { - "isMobile", isMobile - }, - { - "CanDelete", Model.User?.IsModerator ?? false - }, - }) + -
-
- @await Html.PartialAsync("Partials/LeaderboardPartial", ViewData.WithLang(language).WithTime(timeZone).CanDelete(Model.User?.IsModerator ?? false)) -
+ + +
} - @if (isMobile) - { -
- } + +@if (isMobile) +{ +
+} @if (Model.User != null && Model.User.IsModerator) { @@ -227,7 +199,7 @@ else Delete
- + @if (!Model.Slot!.Hidden) { @@ -237,7 +209,7 @@ else
} - + @if (!Model.Slot!.InitiallyLocked && !Model.Slot!.LockedByModerator) { @@ -247,7 +219,7 @@ else
} - + @if (Model.Slot!.CommentsEnabled) { @@ -262,49 +234,7 @@ else } } - \ No newline at end of file +@if (Model.CanViewLevel) +{ + @await Html.PartialAsync("Partials/SectionScriptPartial") +} \ No newline at end of file diff --git a/ProjectLighthouse.Servers.Website/Pages/SlotPage.cshtml.cs b/ProjectLighthouse.Servers.Website/Pages/SlotPage.cshtml.cs index 4fc956b62..064c630c0 100644 --- a/ProjectLighthouse.Servers.Website/Pages/SlotPage.cshtml.cs +++ b/ProjectLighthouse.Servers.Website/Pages/SlotPage.cshtml.cs @@ -1,10 +1,7 @@ #nullable enable -using LBPUnion.ProjectLighthouse.Configuration; using LBPUnion.ProjectLighthouse.Database; using LBPUnion.ProjectLighthouse.Servers.Website.Pages.Layouts; -using LBPUnion.ProjectLighthouse.Types.Entities.Interaction; using LBPUnion.ProjectLighthouse.Types.Entities.Level; -using LBPUnion.ProjectLighthouse.Types.Entities.Profile; using LBPUnion.ProjectLighthouse.Types.Levels; using LBPUnion.ProjectLighthouse.Types.Users; using Microsoft.AspNetCore.Mvc; @@ -14,15 +11,7 @@ namespace LBPUnion.ProjectLighthouse.Servers.Website.Pages; public class SlotPage : BaseLayout { - public Dictionary Comments = new(); - public List Reviews = new(); - public List Photos = new(); - public List Scores = new(); - - public bool CommentsEnabled; - public readonly bool ReviewsEnabled = ServerConfiguration.Instance.UserGeneratedContentLimits.LevelReviewsEnabled; - - public bool CanViewSlot; + public bool CanViewLevel; public SlotEntity? Slot; public SlotPage(DatabaseContext database) : base(database) @@ -33,81 +22,19 @@ public async Task OnGet([FromRoute] int id) SlotEntity? slot = await this.Database.Slots.Include(s => s.Creator) .Where(s => s.Type == SlotType.User || (this.User != null && this.User.PermissionLevel >= PermissionLevel.Moderator)) .FirstOrDefaultAsync(s => s.SlotId == id); - if (slot == null) return this.NotFound(); - System.Diagnostics.Debug.Assert(slot.Creator != null); + if (slot == null || slot.Creator == null) return this.NotFound(); bool isAuthenticated = this.User != null; bool isOwner = slot.Creator == this.User || this.User != null && this.User.IsModerator; // Determine if user can view slot according to creator's privacy settings - this.CanViewSlot = slot.Creator.LevelVisibility.CanAccess(isAuthenticated, isOwner); + this.CanViewLevel = slot.Creator.LevelVisibility.CanAccess(isAuthenticated, isOwner); - if ((slot.Hidden || slot.SubLevel && (this.User == null && this.User != slot.Creator)) && !(this.User?.IsModerator ?? false)) + if ((slot.Hidden || slot.SubLevel && this.User == null && this.User != slot.Creator) && !(this.User?.IsModerator ?? false)) return this.NotFound(); this.Slot = slot; - List blockedUsers = this.User == null - ? new List() - : await ( - from blockedProfile in this.Database.BlockedProfiles - where blockedProfile.UserId == this.User.UserId - select blockedProfile.BlockedUserId).ToListAsync(); - - this.CommentsEnabled = ServerConfiguration.Instance.UserGeneratedContentLimits.LevelCommentsEnabled && this.Slot.CommentsEnabled; - 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) - .Where(c => !blockedUsers.Contains(c.PosterUserId)) - .Include(c => c.Poster) - .Where(c => c.Poster.PermissionLevel != PermissionLevel.Banned) - .Take(50) - .ToDictionaryAsync(c => c, _ => (RatedCommentEntity?)null); - } - else - { - this.Comments = new Dictionary(); - } - - 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) - .Where(r => !blockedUsers.Contains(r.ReviewerId)) - .Take(50) - .ToListAsync(); - } - else - { - this.Reviews = new List(); - } - - this.Photos = await this.Database.Photos.Include(p => p.Creator) - .Include(p => p.PhotoSubjects) - .ThenInclude(ps => ps.User) - .OrderByDescending(p => p.Timestamp) - .Where(r => r.SlotId == id) - .Take(10) - .ToListAsync(); - - this.Scores = await this.Database.Scores.OrderByDescending(s => s.Points) - .ThenBy(s => s.ScoreId) - .Where(s => s.SlotId == id) - .Take(10) - .ToListAsync(); - - if (this.User == null) return this.Page(); - - foreach (KeyValuePair kvp in this.Comments) - { - RatedCommentEntity? reaction = await this.Database.RatedComments.FirstOrDefaultAsync(r => r.UserId == this.User.UserId && r.CommentId == kvp.Key.CommentId); - this.Comments[kvp.Key] = reaction; - } - return this.Page(); } } diff --git a/ProjectLighthouse.Servers.Website/Pages/UserPage.cshtml b/ProjectLighthouse.Servers.Website/Pages/UserPage.cshtml index ffd3d5fb7..d722b3288 100644 --- a/ProjectLighthouse.Servers.Website/Pages/UserPage.cshtml +++ b/ProjectLighthouse.Servers.Website/Pages/UserPage.cshtml @@ -2,9 +2,6 @@ @using System.Web @using LBPUnion.ProjectLighthouse.Extensions @using LBPUnion.ProjectLighthouse.Localization.StringLists -@using LBPUnion.ProjectLighthouse.Servers.Website.Extensions -@using LBPUnion.ProjectLighthouse.Types.Entities.Level -@using LBPUnion.ProjectLighthouse.Types.Entities.Profile @using LBPUnion.ProjectLighthouse.Types.Moderation.Cases @model LBPUnion.ProjectLighthouse.Servers.Website.Pages.UserPage @@ -16,8 +13,6 @@ Model.Description = Model.ProfileUser!.Biography; bool isMobile = Request.IsMobile(); - string language = Model.GetLanguage(); - string timeZone = Model.GetTimeZone(); } @if (Model.ProfileUser.IsBanned) @@ -141,7 +136,7 @@ @if (isMobile) {
- } + } }
@@ -168,7 +163,6 @@ else @Model.Translate(BaseLayoutStrings.HeaderPhotos) - @Model.Translate(BaseLayoutStrings.HeaderSlots) @@ -190,117 +184,38 @@ else string divLength = isMobile ? "sixteen" : "thirteen"; }
-
- @if (Model.ProfileUser.IsBanned) +
+ @for (int i = 0; i < 3; i++) { -
-

- Comments are disabled because the user is banned. -

+
+ @for (int j = 0; j < 5; j++) + { +
+ }
} - else - { - @await Html.PartialAsync("Partials/CommentsPartial", new ViewDataDictionary(ViewData.WithLang(language).WithTime(timeZone)) - { - { - "PageOwner", Model.ProfileUser.UserId - }, - }) - }
-
-
- @if (Model.Photos != null && Model.Photos.Count != 0) - { -
- @foreach (PhotoEntity photo in Model.Photos) - { - string width = isMobile ? "sixteen" : "eight"; - bool canDelete = Model.User != null && (Model.User.IsModerator || Model.User.UserId == photo.CreatorId); -
- @await photo.ToHtml(Html, ViewData, language, timeZone, canDelete) -
- } -
- @if (isMobile) - { -
- } - } - else - { -

This user hasn't uploaded any photos

- } -
+ -
- @if (!Model.CanViewSlots) - { -
-

- The user's privacy settings prevent you from viewing this page. -

-
- } - else - { -
- @if (Model.Slots?.Count == 0) - { -

This user hasn't published any levels

- } - @foreach (SlotEntity slot in Model.Slots ?? new List()) - { -
- @await slot.ToHtml(Html, ViewData, Model.User, $"~/user/{Model.ProfileUser.UserId}#levels", language, timeZone, isMobile, true) -
- } -
- } + -
+ + @if (Model.User == Model.ProfileUser) { -
-
- @if (Model.HeartedSlots?.Count == 0) - { -

You haven't hearted any levels

- } - else - { -

You have hearted @(Model.HeartedSlots?.Count) levels

- } - @foreach (SlotEntity slot in Model.HeartedSlots ?? new List()) - { -
- @await slot.ToHtml(Html, ViewData, Model.User, $"~/user/{Model.ProfileUser.UserId}#hearted", language, timeZone, isMobile, true) -
- } -
+ -
-
- @if (Model.QueuedSlots?.Count == 0) - { -

You haven't queued any levels

- } - else - { -

There are @(Model.QueuedSlots?.Count) levels in your queue

- } - @foreach (SlotEntity slot in Model.QueuedSlots ?? new List()) - { -
- @await slot.ToHtml(Html, ViewData, Model.User, $"~/user/{Model.ProfileUser.UserId}#queued", language, timeZone, isMobile, true) -
- } -
+ }
@@ -322,26 +237,26 @@ else
} - - - - @if (!Model.CommentsDisabledByModerator) + @if (Model.ProfileUser.CommentsEnabled) { } + + + @if (Model.User.IsAdmin) { @await Html.PartialAsync("Partials/AdminSetGrantedSlotsFormPartial", Model.ProfileUser) @@ -353,49 +268,7 @@ else } } - \ No newline at end of file +@if (Model.CanViewProfile) +{ + @await Html.PartialAsync("Partials/SectionScriptPartial") +} \ No newline at end of file diff --git a/ProjectLighthouse.Servers.Website/Pages/UserPage.cshtml.cs b/ProjectLighthouse.Servers.Website/Pages/UserPage.cshtml.cs index 35f7919b1..cf3297785 100644 --- a/ProjectLighthouse.Servers.Website/Pages/UserPage.cshtml.cs +++ b/ProjectLighthouse.Servers.Website/Pages/UserPage.cshtml.cs @@ -1,12 +1,7 @@ #nullable enable -using LBPUnion.ProjectLighthouse.Configuration; using LBPUnion.ProjectLighthouse.Database; using LBPUnion.ProjectLighthouse.Servers.Website.Pages.Layouts; -using LBPUnion.ProjectLighthouse.Types.Entities.Interaction; -using LBPUnion.ProjectLighthouse.Types.Entities.Level; using LBPUnion.ProjectLighthouse.Types.Entities.Profile; -using LBPUnion.ProjectLighthouse.Types.Levels; -using LBPUnion.ProjectLighthouse.Types.Moderation.Cases; using LBPUnion.ProjectLighthouse.Types.Users; using Microsoft.AspNetCore.Mvc; using Microsoft.EntityFrameworkCore; @@ -15,118 +10,32 @@ namespace LBPUnion.ProjectLighthouse.Servers.Website.Pages; public class UserPage : BaseLayout { - public Dictionary Comments = new(); - - public bool CommentsEnabled; - public bool CommentsDisabledByModerator; + public bool CanViewProfile; public bool IsProfileUserHearted; - public bool IsProfileUserBlocked; - public List? Photos; - public List? Slots; - - public List? HeartedSlots; - public List? QueuedSlots; - public UserEntity? ProfileUser; - - public bool CanViewProfile; - public bool CanViewSlots; - public UserPage(DatabaseContext database) : base(database) - { } + {} public async Task OnGet([FromRoute] int userId) { this.ProfileUser = await this.Database.Users.FirstOrDefaultAsync(u => u.UserId == userId); if (this.ProfileUser == null) return this.NotFound(); - bool isAuthenticated = this.User != null; - bool isOwner = this.ProfileUser == this.User || this.User != null && this.User.IsModerator; - - // Determine if user can view profile according to profileUser's privacy settings - this.CanViewProfile = this.ProfileUser.ProfileVisibility.CanAccess(isAuthenticated, isOwner); - this.CanViewSlots = this.ProfileUser.LevelVisibility.CanAccess(isAuthenticated, isOwner); - - this.Photos = await this.Database.Photos.Include(p => p.Slot) - .Include(p => p.PhotoSubjects) - .ThenInclude(ps => ps.User) - .OrderByDescending(p => p.Timestamp) - .Where(p => p.CreatorId == userId) - .Take(6) - .ToListAsync(); - - this.Slots = await this.Database.Slots.Include(p => p.Creator) - .OrderByDescending(s => s.LastUpdated) - .Where(p => p.CreatorId == userId) - .Take(10) - .ToListAsync(); - - if (this.User == this.ProfileUser) - { - this.QueuedSlots = await this.Database.QueuedLevels.Include(h => h.Slot) - .Where(q => this.User != null && q.UserId == this.User.UserId) - .OrderByDescending(q => q.QueuedLevelId) - .Select(q => q.Slot) - .Where(s => s.Type == SlotType.User) - .Take(10) - .ToListAsync(); - this.HeartedSlots = await this.Database.HeartedLevels.Include(h => h.Slot) - .Where(h => this.User != null && h.UserId == this.User.UserId) - .OrderByDescending(h => h.HeartedLevelId) - .Select(h => h.Slot) - .Where(s => s.Type == SlotType.User) - .Take(10) - .ToListAsync(); - } - - this.CommentsEnabled = ServerConfiguration.Instance.UserGeneratedContentLimits.LevelCommentsEnabled && - this.ProfileUser.CommentsEnabled; - - if (this.CommentsEnabled) - { - List blockedUsers = this.User == null - ? new List() - : await ( - from blockedProfile in this.Database.BlockedProfiles - where blockedProfile.UserId == this.User.UserId - select blockedProfile.BlockedUserId).ToListAsync(); - - this.Comments = await this.Database.Comments.Include(p => p.Poster) - .OrderByDescending(p => p.Timestamp) - .Where(p => p.TargetId == userId && p.Type == CommentType.Profile) - .Where(p => !blockedUsers.Contains(p.PosterUserId)) - .Take(50) - .ToDictionaryAsync(c => c, _ => (RatedCommentEntity?)null); - } - else - { - this.Comments = new Dictionary(); - } + this.CanViewProfile = this.ProfileUser.ProfileVisibility.CanAccess(this.User != null, + this.ProfileUser == this.User || this.User != null && this.User.IsModerator); if (this.User == null) return this.Page(); - foreach (KeyValuePair kvp in this.Comments) - { - RatedCommentEntity? reaction = await this.Database.RatedComments.Where(r => r.CommentId == kvp.Key.CommentId) - .Where(r => r.UserId == this.User.UserId) - .FirstOrDefaultAsync(); - this.Comments[kvp.Key] = reaction; - } - - this.IsProfileUserHearted = await this.Database.HeartedProfiles.Where(h => h.HeartedUserId == this.ProfileUser.UserId) + this.IsProfileUserHearted = await this.Database.HeartedProfiles + .Where(h => h.HeartedUserId == this.ProfileUser.UserId) .Where(h => h.UserId == this.User.UserId) .AnyAsync(); this.IsProfileUserBlocked = await this.Database.IsUserBlockedBy(this.ProfileUser.UserId, this.User.UserId); - - this.CommentsDisabledByModerator = await this.Database.Cases.Where(c => c.AffectedId == this.ProfileUser.UserId) - .Where(c => c.Type == CaseType.UserDisableComments) - .Where(c => c.DismissedAt == null) - .AnyAsync(); - + return this.Page(); } } \ No newline at end of file diff --git a/ProjectLighthouse.Servers.Website/Startup/WebsiteStartup.cs b/ProjectLighthouse.Servers.Website/Startup/WebsiteStartup.cs index c391fc5e3..5096928df 100644 --- a/ProjectLighthouse.Servers.Website/Startup/WebsiteStartup.cs +++ b/ProjectLighthouse.Servers.Website/Startup/WebsiteStartup.cs @@ -38,6 +38,16 @@ public void ConfigureServices(IServiceCollection services) { services.AddControllers(); #if DEBUG + // Add CORS for debugging + services.AddCors(options => + { + options.AddDefaultPolicy(policy => + { + policy.AllowAnyHeader(); + policy.AllowAnyMethod(); + policy.AllowAnyOrigin(); + }); + }); services.AddRazorPages().WithRazorPagesAtContentRoot().AddRazorRuntimeCompilation((options) => { // jank but works @@ -112,6 +122,9 @@ public virtual void Configure(IApplicationBuilder app, IWebHostEnvironment env) { #if DEBUG app.UseDeveloperExceptionPage(); + app.UseCors(); + #else + app.UseExceptionHandler("/error"); #endif app.UseStatusCodePagesWithReExecute("/404"); @@ -131,6 +144,8 @@ public virtual void Configure(IApplicationBuilder app, IWebHostEnvironment env) app.UseRequestLocalization(); + app.UseResponseCaching(); + app.UseEndpoints(endpoints => { endpoints.MapControllers(); diff --git a/ProjectLighthouse/Database/DatabaseContext.GameTokens.cs b/ProjectLighthouse/Database/DatabaseContext.GameTokens.cs index 2ed62d8ef..da7570af1 100644 --- a/ProjectLighthouse/Database/DatabaseContext.GameTokens.cs +++ b/ProjectLighthouse/Database/DatabaseContext.GameTokens.cs @@ -2,6 +2,8 @@ using System; using System.Linq; using System.Threading.Tasks; +using LBPUnion.ProjectLighthouse.Helpers; +using LBPUnion.ProjectLighthouse.Tickets; using LBPUnion.ProjectLighthouse.Types.Entities.Profile; using LBPUnion.ProjectLighthouse.Types.Entities.Token; using Microsoft.AspNetCore.Http; @@ -11,6 +13,28 @@ namespace LBPUnion.ProjectLighthouse.Database; public partial class DatabaseContext { + public async Task AuthenticateUser(UserEntity? user, NPTicket npTicket, string userLocation) + { + if (user == null) return null; + + GameTokenEntity gameToken = new() + { + UserToken = CryptoHelper.GenerateAuthToken(), + User = user, + UserId = user.UserId, + UserLocation = userLocation, + GameVersion = npTicket.GameVersion, + Platform = npTicket.Platform, + TicketHash = npTicket.TicketHash, + // we can get away with a low expiry here since LBP will just get a new token everytime it gets 403'd + ExpiresAt = DateTime.Now + TimeSpan.FromHours(1), + }; + + this.GameTokens.Add(gameToken); + await this.SaveChangesAsync(); + + return gameToken; + } public async Task UsernameFromGameToken(GameTokenEntity? token) { diff --git a/ProjectLighthouse/Database/DatabaseContext.Utils.cs b/ProjectLighthouse/Database/DatabaseContext.User.cs similarity index 74% rename from ProjectLighthouse/Database/DatabaseContext.Utils.cs rename to ProjectLighthouse/Database/DatabaseContext.User.cs index 939f978df..545553841 100644 --- a/ProjectLighthouse/Database/DatabaseContext.Utils.cs +++ b/ProjectLighthouse/Database/DatabaseContext.User.cs @@ -1,16 +1,16 @@ -using System; +#nullable enable +using System; +using System.Collections.Generic; using System.Linq; using System.Text.RegularExpressions; using System.Threading.Tasks; using LBPUnion.ProjectLighthouse.Configuration; using LBPUnion.ProjectLighthouse.Extensions; using LBPUnion.ProjectLighthouse.Helpers; -using LBPUnion.ProjectLighthouse.Tickets; using LBPUnion.ProjectLighthouse.Types.Entities.Interaction; using LBPUnion.ProjectLighthouse.Types.Entities.Level; using LBPUnion.ProjectLighthouse.Types.Entities.Moderation; using LBPUnion.ProjectLighthouse.Types.Entities.Profile; -using LBPUnion.ProjectLighthouse.Types.Entities.Token; using Microsoft.EntityFrameworkCore; namespace LBPUnion.ProjectLighthouse.Database; @@ -30,9 +30,11 @@ public async Task CreateUser(string username, string password, strin // 16 is PSN max, 3 is PSN minimum if (!ServerStatics.IsUnitTesting || !username.StartsWith("unitTestUser")) { - if (username.Length is > 16 or < 3) throw new ArgumentException(nameof(username) + " is either too long or too short"); + if (username.Length is > 16 or < 3) + throw new ArgumentException(nameof(username) + " is either too long or too short"); - if (!this.IsUsernameValid(username)) throw new ArgumentException(nameof(username) + " does not match the username regex"); + if (!this.IsUsernameValid(username)) + throw new ArgumentException(nameof(username) + " does not match the username regex"); } UserEntity? user = await this.Users.Where(u => u.Username == username).FirstOrDefaultAsync(); @@ -58,29 +60,6 @@ public async Task UserIdFromUsername(string? username) return await this.Users.Where(u => u.Username == username).Select(u => u.UserId).FirstOrDefaultAsync(); } - public async Task AuthenticateUser(UserEntity? user, NPTicket npTicket, string userLocation) - { - if (user == null) return null; - - GameTokenEntity gameToken = new() - { - UserToken = CryptoHelper.GenerateAuthToken(), - User = user, - UserId = user.UserId, - UserLocation = userLocation, - GameVersion = npTicket.GameVersion, - Platform = npTicket.Platform, - TicketHash = npTicket.TicketHash, - // we can get away with a low expiry here since LBP will just get a new token everytime it gets 403'd - ExpiresAt = DateTime.Now + TimeSpan.FromHours(1), - }; - - this.GameTokens.Add(gameToken); - await this.SaveChangesAsync(); - - return gameToken; - } - public async Task RemoveUser(UserEntity? user) { if (user == null) return; @@ -93,13 +72,12 @@ public async Task RemoveUser(UserEntity? user) .Where(c => c.CreatorId == user.UserId || c.DismisserId == user.UserId) .ToListAsync()) { - if(modCase.DismisserId == user.UserId) - modCase.DismisserId = null; - if(modCase.CreatorId == user.UserId) - modCase.CreatorId = await SlotHelper.GetPlaceholderUserId(this); + if (modCase.DismisserId == user.UserId) modCase.DismisserId = null; + if (modCase.CreatorId == user.UserId) modCase.CreatorId = await SlotHelper.GetPlaceholderUserId(this); } - foreach (SlotEntity slot in this.Slots.Where(s => s.CreatorId == user.UserId)) await this.RemoveSlot(slot, false); + foreach (SlotEntity slot in this.Slots.Where(s => s.CreatorId == user.UserId)) + await this.RemoveSlot(slot, false); this.HeartedProfiles.RemoveRange(this.HeartedProfiles.Where(h => h.UserId == user.UserId)); this.PhotoSubjects.RemoveRange(this.PhotoSubjects.Where(s => s.UserId == user.UserId)); @@ -124,7 +102,9 @@ public async Task HeartUser(int userId, UserEntity heartedUser) { if (userId == heartedUser.UserId) return; - HeartedProfileEntity? heartedProfile = await this.HeartedProfiles.FirstOrDefaultAsync(q => q.UserId == userId && q.HeartedUserId == heartedUser.UserId); + HeartedProfileEntity? heartedProfile = + await this.HeartedProfiles.FirstOrDefaultAsync(q => + q.UserId == userId && q.HeartedUserId == heartedUser.UserId); if (heartedProfile != null) return; this.HeartedProfiles.Add(new HeartedProfileEntity @@ -138,7 +118,9 @@ public async Task HeartUser(int userId, UserEntity heartedUser) public async Task UnheartUser(int userId, UserEntity heartedUser) { - HeartedProfileEntity? heartedProfile = await this.HeartedProfiles.FirstOrDefaultAsync(q => q.UserId == userId && q.HeartedUserId == heartedUser.UserId); + HeartedProfileEntity? heartedProfile = + await this.HeartedProfiles.FirstOrDefaultAsync(q => + q.UserId == userId && q.HeartedUserId == heartedUser.UserId); if (heartedProfile != null) this.HeartedProfiles.Remove(heartedProfile); await this.SaveChangesAsync(); @@ -168,11 +150,17 @@ public async Task UnblockUser(int userId, UserEntity blockedUser) await this.BlockedProfiles.RemoveWhere(bp => bp.BlockedUserId == blockedUser.UserId && bp.UserId == userId); } + public Task> GetBlockedUsers(int? userId) + { + return userId == null + ? Task.FromResult(new List()) + : this.BlockedProfiles.Where(b => b.UserId == userId).Select(b => b.BlockedUserId).ToListAsync(); + } + public async Task IsUserBlockedBy(int userId, int targetId) { if (targetId == userId) return false; return await this.BlockedProfiles.Has(bp => bp.BlockedUserId == userId && bp.UserId == targetId); } - } \ No newline at end of file diff --git a/ProjectLighthouse/StaticFiles/css/styles.css b/ProjectLighthouse/StaticFiles/css/styles.css index 0f9aa8465..060ba9541 100644 --- a/ProjectLighthouse/StaticFiles/css/styles.css +++ b/ProjectLighthouse/StaticFiles/css/styles.css @@ -155,6 +155,15 @@ div.cardStatsUnderTitle > span { /*#endregion Cards*/ +/* #region Profiles & Slots */ + +.hidden { + visibility: hidden; + max-height: 0; +} + +/* #endregion Profiles & Slots */ + /* #region Two Factor */ .digits input { font-size: 2rem; diff --git a/ProjectLighthouse/Types/Entities/Level/ReviewEntity.cs b/ProjectLighthouse/Types/Entities/Level/ReviewEntity.cs index 59091953c..a20b23acf 100644 --- a/ProjectLighthouse/Types/Entities/Level/ReviewEntity.cs +++ b/ProjectLighthouse/Types/Entities/Level/ReviewEntity.cs @@ -1,4 +1,3 @@ -#nullable enable using System; using System.ComponentModel.DataAnnotations; using System.ComponentModel.DataAnnotations.Schema; @@ -15,12 +14,12 @@ public class ReviewEntity public int ReviewerId { get; set; } [ForeignKey(nameof(ReviewerId))] - public UserEntity? Reviewer { get; set; } + public UserEntity Reviewer { get; set; } public int SlotId { get; set; } [ForeignKey(nameof(SlotId))] - public SlotEntity? Slot { get; set; } + public SlotEntity Slot { get; set; } public long Timestamp { get; set; } diff --git a/ProjectLighthouse/Types/Entities/Profile/CommentEntity.cs b/ProjectLighthouse/Types/Entities/Profile/CommentEntity.cs index ec2cd6405..14bb50769 100644 --- a/ProjectLighthouse/Types/Entities/Profile/CommentEntity.cs +++ b/ProjectLighthouse/Types/Entities/Profile/CommentEntity.cs @@ -50,9 +50,11 @@ public string GetCommentMessage(DatabaseContext database) return "This comment has been deleted by the author."; } - UserEntity deletedBy = database.Users.FirstOrDefault(u => u.Username == this.DeletedBy); + int deletedById = database.Users.Where(u => u.Username == this.DeletedBy) + .Select(u => u.UserId) + .FirstOrDefault(); - if (deletedBy != null && deletedBy.UserId == this.TargetId) + if (deletedById != 0 && deletedById == this.TargetId) { return "This comment has been deleted by the player."; } diff --git a/ProjectLighthouse/Types/Serialization/GamePhotoSubject.cs b/ProjectLighthouse/Types/Serialization/GamePhotoSubject.cs index 94cbd50cb..941925c99 100644 --- a/ProjectLighthouse/Types/Serialization/GamePhotoSubject.cs +++ b/ProjectLighthouse/Types/Serialization/GamePhotoSubject.cs @@ -1,4 +1,5 @@ -using System.Xml.Serialization; +using System.ComponentModel; +using System.Xml.Serialization; using LBPUnion.ProjectLighthouse.Types.Entities.Profile; namespace LBPUnion.ProjectLighthouse.Types.Serialization; @@ -18,6 +19,7 @@ public class GamePhotoSubject : ILbpSerializable public string DisplayName => this.Username; [XmlElement("bounds")] + [DefaultValue(null)] public string Bounds { get; set; } public static GamePhotoSubject CreateFromEntity(PhotoSubjectEntity entity) => diff --git a/ProjectLighthouse/Types/Users/PrivacyType.cs b/ProjectLighthouse/Types/Users/PrivacyType.cs index d9122a389..d9562b4b9 100644 --- a/ProjectLighthouse/Types/Users/PrivacyType.cs +++ b/ProjectLighthouse/Types/Users/PrivacyType.cs @@ -49,13 +49,13 @@ public static string ToSerializedString(this PrivacyType type) }; } - public static bool CanAccess(this PrivacyType type, bool authenticated, bool owner) + public static bool CanAccess(this PrivacyType type, bool isAuthenticated, bool isOwner) { return type switch { PrivacyType.All => true, - PrivacyType.PSN => authenticated, - PrivacyType.Game => authenticated && owner, + PrivacyType.PSN => isAuthenticated, + PrivacyType.Game => isAuthenticated && isOwner, _ => false, }; }