diff --git a/.config/dotnet-tools.json b/.config/dotnet-tools.json index a1b89d98..7217a983 100644 --- a/.config/dotnet-tools.json +++ b/.config/dotnet-tools.json @@ -3,7 +3,7 @@ "isRoot": true, "tools": { "dotnet-ef": { - "version": "8.0.3", + "version": "8.0.6", "commands": [ "dotnet-ef" ] diff --git a/ProjectLighthouse.Servers.GameServer/Controllers/Slots/ReviewController.cs b/ProjectLighthouse.Servers.GameServer/Controllers/Slots/ReviewController.cs index 7feae4bf..0dd54c07 100644 --- a/ProjectLighthouse.Servers.GameServer/Controllers/Slots/ReviewController.cs +++ b/ProjectLighthouse.Servers.GameServer/Controllers/Slots/ReviewController.cs @@ -157,6 +157,13 @@ public class ReviewController : ControllerBase List reviews = (await this.database.Reviews .Where(r => r.SlotId == slotId) + .Select(r => new + { + Review = r, + SlotVersion = r.Slot!.GameVersion, + }) + .Where(a => a.SlotVersion <= token.GameVersion) + .Select(a => a.Review) .OrderByDescending(r => r.ThumbsUp - r.ThumbsDown) .ThenByDescending(r => r.Timestamp) .ApplyPagination(pageData) @@ -178,6 +185,13 @@ public class ReviewController : ControllerBase List reviews = (await this.database.Reviews .Where(r => r.ReviewerId == targetUserId) + .Select(r => new + { + Review = r, + SlotVersion = r.Slot!.GameVersion, + }) + .Where(a => a.SlotVersion <= token.GameVersion) + .Select(a => a.Review) .OrderByDescending(r => r.Timestamp) .ApplyPagination(pageData) .ToListAsync()).ToSerializableList(r => GameReview.CreateFromEntity(r, token)); diff --git a/ProjectLighthouse.Servers.Website/Controllers/Admin/AdminUserController.cs b/ProjectLighthouse.Servers.Website/Controllers/Admin/AdminUserController.cs index 84b024ff..55db420c 100644 --- a/ProjectLighthouse.Servers.Website/Controllers/Admin/AdminUserController.cs +++ b/ProjectLighthouse.Servers.Website/Controllers/Admin/AdminUserController.cs @@ -1,8 +1,11 @@ #nullable enable using LBPUnion.ProjectLighthouse.Database; +using LBPUnion.ProjectLighthouse.Extensions; using LBPUnion.ProjectLighthouse.Files; using LBPUnion.ProjectLighthouse.Logging; +using LBPUnion.ProjectLighthouse.Types.Entities.Level; using LBPUnion.ProjectLighthouse.Types.Entities.Profile; +using LBPUnion.ProjectLighthouse.Types.Entities.Token; using LBPUnion.ProjectLighthouse.Types.Logging; using LBPUnion.ProjectLighthouse.Types.Moderation.Cases; using LBPUnion.ProjectLighthouse.Types.Users; @@ -26,7 +29,8 @@ public class AdminUserController : ControllerBase /// Resets the user's earth decorations to a blank state. Useful for users who abuse audio for example. /// [HttpGet("wipePlanets")] - public async Task WipePlanets([FromRoute] int id) { + public async Task WipePlanets([FromRoute] int id) + { UserEntity? user = this.database.UserFromWebRequest(this.Request); if (user == null || !user.IsModerator) return this.NotFound(); @@ -90,6 +94,78 @@ public class AdminUserController : ControllerBase return this.Redirect($"/user/{targetedUser.UserId}"); } + + /// + /// Deletes every comment by the user. Useful in case of mass spam + /// + [HttpGet("wipeComments")] + public async Task WipeComments([FromRoute] int id) + { + UserEntity? user = this.database.UserFromWebRequest(this.Request); + if (user == null || !user.IsModerator) return this.NotFound(); + + UserEntity? targetedUser = await this.database.Users.FirstOrDefaultAsync(u => u.UserId == id); + if (targetedUser == null) return this.NotFound(); + + // Find every comment by the user, then set the deletion info on them + await this.database.Comments.Where(c => c.PosterUserId == targetedUser.UserId) + .ExecuteUpdateAsync(s => + s.SetProperty(c => c.Deleted, true) + .SetProperty(c => c.DeletedBy, user.Username) + .SetProperty(c => c.DeletedType, "moderator")); + Logger.Success($"Deleted comments for {targetedUser.Username} (id:{targetedUser.UserId})", LogArea.Admin); + + await this.database.SendNotification(targetedUser.UserId, + "Your comments have been deleted by a moderator."); + + return this.Redirect($"/user/{targetedUser.UserId}"); + } + + /// + /// Deletes every score from the user. Useful in the case where a user cheated a ton of scores + /// + [HttpGet("wipeScores")] + public async Task WipeScores([FromRoute] int id) + { + UserEntity? user = this.database.UserFromWebRequest(this.Request); + if (user == null || !user.IsModerator) return this.NotFound(); + + UserEntity? targetedUser = await this.database.Users.FirstOrDefaultAsync(u => u.UserId == id); + if (targetedUser == null) return this.NotFound(); + + // Find and delete every score uploaded by the target user + await this.database.Scores.Where(c => c.UserId == targetedUser.UserId).ExecuteDeleteAsync(); + Logger.Success($"Deleted scores for {targetedUser.Username} (id:{targetedUser.UserId})", LogArea.Admin); + + await this.database.SendNotification(targetedUser.UserId, "Your scores have been deleted by a moderator."); + + return this.Redirect($"/user/{targetedUser.UserId}"); + } + + /// + /// Forces the email verification of a user. + /// + [HttpGet("forceVerifyEmail")] + public async Task ForceVerifyEmail([FromRoute] int id) + { + UserEntity? user = this.database.UserFromWebRequest(this.Request); + if (user == null || !user.IsModerator) return this.NotFound(); + + UserEntity? targetedUser = await this.database.Users.FirstOrDefaultAsync(u => u.UserId == id); + if (targetedUser == null) return this.NotFound(); + if (targetedUser.EmailAddress == null || targetedUser.EmailAddressVerified) return this.NotFound(); + + List tokens = await this.database.EmailVerificationTokens + .Where(t => t.UserId == targetedUser.UserId) + .ToListAsync(); + this.database.EmailVerificationTokens.RemoveRange(tokens); + + targetedUser.EmailAddressVerified = true; + + await this.database.SaveChangesAsync(); + + return this.Redirect($"/user/{targetedUser.UserId}"); + } [HttpPost("/admin/user/{id:int}/setPermissionLevel")] public async Task SetUserPermissionLevel([FromRoute] int id, [FromForm] PermissionLevel role) diff --git a/ProjectLighthouse.Servers.Website/Pages/UserPage.cshtml b/ProjectLighthouse.Servers.Website/Pages/UserPage.cshtml index d675e8c1..98c23fcd 100644 --- a/ProjectLighthouse.Servers.Website/Pages/UserPage.cshtml +++ b/ProjectLighthouse.Servers.Website/Pages/UserPage.cshtml @@ -324,6 +324,18 @@ else Wipe Earth Decorations + + + + Wipe User's Comments + + + + + Wipe User's Scores + @if (!Model.CommentsDisabledByModerator) { @@ -333,6 +345,14 @@ else } + @if (Model.ProfileUser.EmailAddress != null && !Model.ProfileUser.EmailAddressVerified) + { + + + Forcibly Verify Email + + } + @if (Model.User.IsAdmin) {
diff --git a/ProjectLighthouse.Servers.Website/ProjectLighthouse.Servers.Website.csproj b/ProjectLighthouse.Servers.Website/ProjectLighthouse.Servers.Website.csproj index 9749d85c..7768663f 100644 --- a/ProjectLighthouse.Servers.Website/ProjectLighthouse.Servers.Website.csproj +++ b/ProjectLighthouse.Servers.Website/ProjectLighthouse.Servers.Website.csproj @@ -11,6 +11,6 @@ - + diff --git a/ProjectLighthouse.Tests.GameApiTests/ProjectLighthouse.Tests.GameApiTests.csproj b/ProjectLighthouse.Tests.GameApiTests/ProjectLighthouse.Tests.GameApiTests.csproj index db0ead10..ef720919 100644 --- a/ProjectLighthouse.Tests.GameApiTests/ProjectLighthouse.Tests.GameApiTests.csproj +++ b/ProjectLighthouse.Tests.GameApiTests/ProjectLighthouse.Tests.GameApiTests.csproj @@ -9,14 +9,14 @@ - - + + all runtime; build; native; contentfiles; analyzers; buildtransitive - - - + + + runtime; build; native; contentfiles; analyzers; buildtransitive all diff --git a/ProjectLighthouse.Tests.GameApiTests/Unit/Controllers/ReviewControllerTests.cs b/ProjectLighthouse.Tests.GameApiTests/Unit/Controllers/ReviewControllerTests.cs new file mode 100644 index 00000000..303b4f85 --- /dev/null +++ b/ProjectLighthouse.Tests.GameApiTests/Unit/Controllers/ReviewControllerTests.cs @@ -0,0 +1,99 @@ +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using LBPUnion.ProjectLighthouse.Database; +using LBPUnion.ProjectLighthouse.Servers.GameServer.Controllers.Slots; +using LBPUnion.ProjectLighthouse.Tests.Helpers; +using LBPUnion.ProjectLighthouse.Types.Entities.Level; +using LBPUnion.ProjectLighthouse.Types.Entities.Token; +using LBPUnion.ProjectLighthouse.Types.Serialization; +using LBPUnion.ProjectLighthouse.Types.Users; +using Microsoft.AspNetCore.Mvc; +using Xunit; + +namespace ProjectLighthouse.Tests.GameApiTests.Unit.Controllers; + +public class ReviewControllerTests +{ + private static async Task InsertTestData(DatabaseContext database) + { + database.Slots.Add(new SlotEntity + { + SlotId = 1, + CreatorId = 1, + GameVersion = GameVersion.LittleBigPlanet3, + }); + + database.Slots.Add(new SlotEntity + { + SlotId = 2, + CreatorId = 1, + GameVersion = GameVersion.LittleBigPlanet2, + }); + + database.Reviews.Add(new ReviewEntity + { + ReviewId = 1, + ReviewerId = 1, + SlotId = 1, + }); + + database.Reviews.Add(new ReviewEntity + { + ReviewId = 2, + ReviewerId = 1, + SlotId = 2, + }); + await database.SaveChangesAsync(); + } + + [Theory] + [InlineData(GameVersion.LittleBigPlanet2, 1)] + [InlineData(GameVersion.LittleBigPlanet3, 2)] + public async Task ReviewsBy_ShouldNotList_HigherGameVersions(GameVersion version, int expected) + { + GameTokenEntity token = MockHelper.GetUnitTestToken(); + token.GameVersion = version; + DatabaseContext database = await MockHelper.GetTestDatabase(new List + { + token, + }); + + await InsertTestData(database); + + ReviewController controller = new(database); + controller.SetupTestController(token); + + IActionResult response = await controller.ReviewsBy("unittest"); + ReviewResponse review = response.CastTo(); + + Assert.Equal(expected, review.Reviews.Count); + Assert.True(review.Reviews.All(r => database.Slots.FirstOrDefault(s => s.SlotId == r.Slot.SlotId)?.GameVersion <= version)); + } + + [Theory] + [InlineData(GameVersion.LittleBigPlanet2, 2, 1)] + [InlineData(GameVersion.LittleBigPlanet2, 1, 0)] + [InlineData(GameVersion.LittleBigPlanet3, 2, 1)] + [InlineData(GameVersion.LittleBigPlanet3, 1, 1)] + public async Task ReviewsFor_ShouldNotList_HigherGameVersions(GameVersion version, int slotId, int expected) + { + GameTokenEntity token = MockHelper.GetUnitTestToken(); + token.GameVersion = version; + DatabaseContext database = await MockHelper.GetTestDatabase(new List + { + token, + }); + + await InsertTestData(database); + + ReviewController controller = new(database); + controller.SetupTestController(token); + + IActionResult response = await controller.ReviewsFor(slotId); + ReviewResponse review = response.CastTo(); + + Assert.Equal(expected, review.Reviews.Count); + Assert.True(review.Reviews.All(r => database.Slots.FirstOrDefault(s => s.SlotId == r.Slot.SlotId)?.GameVersion <= version)); + } +} \ No newline at end of file diff --git a/ProjectLighthouse.Tests.WebsiteTests/ProjectLighthouse.Tests.WebsiteTests.csproj b/ProjectLighthouse.Tests.WebsiteTests/ProjectLighthouse.Tests.WebsiteTests.csproj index 0686a1d3..c5b336f5 100644 --- a/ProjectLighthouse.Tests.WebsiteTests/ProjectLighthouse.Tests.WebsiteTests.csproj +++ b/ProjectLighthouse.Tests.WebsiteTests/ProjectLighthouse.Tests.WebsiteTests.csproj @@ -9,16 +9,16 @@ - - + + all runtime; build; native; contentfiles; analyzers; buildtransitive - - - - - + + + + + runtime; build; native; contentfiles; analyzers; buildtransitive all diff --git a/ProjectLighthouse.Tests/ProjectLighthouse.Tests.csproj b/ProjectLighthouse.Tests/ProjectLighthouse.Tests.csproj index 4eb6d411..4c4199a9 100644 --- a/ProjectLighthouse.Tests/ProjectLighthouse.Tests.csproj +++ b/ProjectLighthouse.Tests/ProjectLighthouse.Tests.csproj @@ -14,14 +14,14 @@ - - + + all runtime; build; native; contentfiles; analyzers; buildtransitive - - - + + + runtime; build; native; contentfiles; analyzers; buildtransitive all @@ -30,7 +30,7 @@ all - + diff --git a/ProjectLighthouse.sln.DotSettings b/ProjectLighthouse.sln.DotSettings index d3818928..174ff7a3 100644 --- a/ProjectLighthouse.sln.DotSettings +++ b/ProjectLighthouse.sln.DotSettings @@ -1,4 +1,6 @@ - + True False False @@ -96,10 +98,14 @@ <Policy Inspect="True" Prefix="" Suffix="" Style="AaBb"><ExtraRule Prefix="" Suffix="" Style="aaBb" /></Policy> <Policy Inspect="True" Prefix="" Suffix="" Style="aaBb" /> <Policy Inspect="True" Prefix="" Suffix="" Style="aaBb" /> + <Policy><Descriptor Staticness="Static" AccessRightKinds="Private" Description="Static readonly fields (private)"><ElementKinds><Kind Name="READONLY_FIELD" /></ElementKinds></Descriptor><Policy Inspect="True" Prefix="" Suffix="" Style="aaBb" /></Policy> + <Policy><Descriptor Staticness="Any" AccessRightKinds="Private" Description="Constant fields (private)"><ElementKinds><Kind Name="CONSTANT_FIELD" /></ElementKinds></Descriptor><Policy Inspect="True" Prefix="" Suffix="" Style="aaBb" /></Policy> + <Policy><Descriptor Staticness="Any" AccessRightKinds="Any" Description="Methods"><ElementKinds><Kind Name="METHOD" /></ElementKinds></Descriptor><Policy Inspect="True" Prefix="" Suffix="" Style="AaBb"><ExtraRule Prefix="" Suffix="" Style="aaBb" /></Policy></Policy> True True True True + True True True True diff --git a/ProjectLighthouse/ProjectLighthouse.csproj b/ProjectLighthouse/ProjectLighthouse.csproj index bc92bc90..c538b220 100644 --- a/ProjectLighthouse/ProjectLighthouse.csproj +++ b/ProjectLighthouse/ProjectLighthouse.csproj @@ -10,31 +10,31 @@ - + all runtime; build; native; contentfiles; analyzers; buildtransitive - + - - - + + + all runtime; build; native; contentfiles; analyzers; buildtransitive - + - - - + + + - + diff --git a/ProjectLighthouse/Tickets/TicketReader.cs b/ProjectLighthouse/Tickets/TicketReader.cs index ee6b1a70..697a72a1 100644 --- a/ProjectLighthouse/Tickets/TicketReader.cs +++ b/ProjectLighthouse/Tickets/TicketReader.cs @@ -33,6 +33,9 @@ public struct SectionHeader public SectionType Type; public ushort Length; public int Position; + + public override string ToString() => + $"SectionHeader(Type='{this.Type}', Length='{this.Length}', Position='{this.Position}')"; } public class TicketReader : BinaryReader