From 116273d1415c26b0aeb34d127e5d00435c27dc85 Mon Sep 17 00:00:00 2001 From: Toaster2 Date: Wed, 15 Apr 2026 20:09:39 +0200 Subject: [PATCH 1/4] Make all disallow tables store timestamps and reasons, return any disallow entity when adding to DB --- .../GameDatabaseContext.Assets.cs | 2 - .../GameDatabaseContext.Registration.cs | 114 +++++++++++------- ...0_AddReasonAndTimestampToDisallowTables.cs | 88 ++++++++++++++ .../GameDatabaseContextModelSnapshot.cs | 18 +++ .../Models/Users/DisallowedEmailAddress.cs | 2 + .../Models/Users/DisallowedEmailDomain.cs | 2 + .../Models/Users/DisallowedUser.cs | 2 + Refresh.GameServer/CommandLineManager.cs | 6 +- Refresh.GameServer/RefreshGameServer.cs | 15 ++- .../Tests/ApiV3/UserApiTests.cs | 6 +- 10 files changed, 195 insertions(+), 60 deletions(-) create mode 100644 Refresh.Database/Migrations/20260415112120_AddReasonAndTimestampToDisallowTables.cs diff --git a/Refresh.Database/GameDatabaseContext.Assets.cs b/Refresh.Database/GameDatabaseContext.Assets.cs index 17611405..c6cb8257 100644 --- a/Refresh.Database/GameDatabaseContext.Assets.cs +++ b/Refresh.Database/GameDatabaseContext.Assets.cs @@ -117,8 +117,6 @@ public void SetMainlinePhotoHash(GameAsset asset, string hash) => /// /// The asset's disallowance info + whether the asset wasn't already disallowed before /// this.DisallowedUsers.Any(u => u.Username == username); + + public DisallowedUser? GetDisallowedUserInfo(string username) + => this.DisallowedUsers.FirstOrDefault(d => d.Username == username); + + public DatabaseList GetDisallowedUsers(int skip, int count) + => new(this.DisallowedUsers, skip, count); - public bool DisallowUser(string username) + public (DisallowedUser, bool) DisallowUser(string username, string reason) { - if (this.DisallowedUsers.FirstOrDefault(u => u.Username == username) != null) - return false; + DisallowedUser? existing = this.GetDisallowedUserInfo(username); + if (existing != null) return (existing, false); - this.Write(() => + DisallowedUser disallowed = new() { - this.DisallowedUsers.Add(new DisallowedUser - { - Username = username, - }); - }); + Username = username, + Reason = reason, + DisallowedAt = this._time.Now, + }; + this.DisallowedUsers.Add(disallowed); + this.SaveChanges(); - return true; + return (disallowed, true); } public bool ReallowUser(string username) { - DisallowedUser? disallowedUser = this.DisallowedUsers.FirstOrDefault(u => u.Username == username); + DisallowedUser? disallowedUser = this.GetDisallowedUserInfo(username); if (disallowedUser == null) return false; - this.Write(() => - { - this.DisallowedUsers.Remove(disallowedUser); - }); + this.DisallowedUsers.Remove(disallowedUser); + this.SaveChanges(); return true; } - - public bool IsUserDisallowed(string username) - { - return this.DisallowedUsers.FirstOrDefault(u => u.Username == username) != null; - } - public bool DisallowEmailAddress(string emailAddress) + public bool IsEmailAddressDisallowed(string emailAddress) + => this.DisallowedEmailAddresses.Any(u => u.Address == emailAddress); + + public DisallowedEmailAddress? GetDisallowedEmailAddressInfo(string emailAddress) + => this.DisallowedEmailAddresses.FirstOrDefault(d => d.Address == emailAddress); + + public DatabaseList GetDisallowedEmailAddresses(int skip, int count) + => new(this.DisallowedEmailAddresses, skip, count); + + public (DisallowedEmailAddress, bool) DisallowEmailAddress(string emailAddress, string reason) { - if (this.IsEmailAddressDisallowed(emailAddress)) - return false; + DisallowedEmailAddress? existing = this.GetDisallowedEmailAddressInfo(emailAddress); + if (existing != null) return (existing, false); - this.DisallowedEmailAddresses.Add(new() + DisallowedEmailAddress disallowed = new() { Address = emailAddress, - }); + Reason = reason, + DisallowedAt = this._time.Now, + }; + this.DisallowedEmailAddresses.Add(disallowed); this.SaveChanges(); - return true; + return (disallowed, true); } public bool ReallowEmailAddress(string emailAddress) { - DisallowedEmailAddress? DisallowedEmailAddress = this.DisallowedEmailAddresses.FirstOrDefault(u => u.Address == emailAddress); - if (DisallowedEmailAddress == null) + DisallowedEmailAddress? disallowed = this.GetDisallowedEmailAddressInfo(emailAddress); + if (disallowed == null) return false; - this.DisallowedEmailAddresses.Remove(DisallowedEmailAddress); + this.DisallowedEmailAddresses.Remove(disallowed); this.SaveChanges(); return true; } + + private string GetEmailDomainFromAddress(string emailAddress) + => emailAddress.Split('@').Last(); - public bool IsEmailAddressDisallowed(string emailAddress) + public bool IsEmailDomainDisallowed(string emailAddress) { - return this.DisallowedEmailAddresses.Any(u => u.Address == emailAddress); + string emailDomain = this.GetEmailDomainFromAddress(emailAddress); + return this.DisallowedEmailDomains.Any(u => u.Domain == emailDomain); } - private string GetEmailDomainFromAddress(string emailAddress) - => emailAddress.Split('@').Last(); + public DisallowedEmailDomain? GetDisallowedEmailDomainInfo(string emailAddress) + { + string emailDomain = this.GetEmailDomainFromAddress(emailAddress); + return this.DisallowedEmailDomains.FirstOrDefault(d => d.Domain == emailDomain); + } - public bool DisallowEmailDomain(string emailAddress) + public DatabaseList GetDisallowedEmailDomains(int skip, int count) + => new(this.DisallowedEmailDomains, skip, count); + + public (DisallowedEmailDomain, bool) DisallowEmailDomain(string emailAddress, string reason) { string emailDomain = this.GetEmailDomainFromAddress(emailAddress); - if (this.IsEmailDomainDisallowed(emailDomain)) - return false; + DisallowedEmailDomain? existing = this.GetDisallowedEmailDomainInfo(emailDomain); + if (existing != null) return (existing, false); - this.DisallowedEmailDomains.Add(new() + DisallowedEmailDomain disallowed = new() { Domain = emailDomain, - }); + Reason = reason, + DisallowedAt = this._time.Now, + }; + this.DisallowedEmailDomains.Add(disallowed); this.SaveChanges(); - return true; + return (disallowed, true); } public bool ReallowEmailDomain(string emailAddress) { string emailDomain = this.GetEmailDomainFromAddress(emailAddress); - DisallowedEmailDomain? disallowedDomain = this.DisallowedEmailDomains.FirstOrDefault(u => u.Domain == emailDomain); + DisallowedEmailDomain? disallowedDomain = this.GetDisallowedEmailDomainInfo(emailDomain); if (disallowedDomain == null) return false; @@ -314,10 +342,4 @@ public bool ReallowEmailDomain(string emailAddress) return true; } - - public bool IsEmailDomainDisallowed(string emailAddress) - { - string emailDomain = this.GetEmailDomainFromAddress(emailAddress); - return this.DisallowedEmailDomains.Any(u => u.Domain == emailDomain); - } } \ No newline at end of file diff --git a/Refresh.Database/Migrations/20260415112120_AddReasonAndTimestampToDisallowTables.cs b/Refresh.Database/Migrations/20260415112120_AddReasonAndTimestampToDisallowTables.cs new file mode 100644 index 00000000..d851d5c5 --- /dev/null +++ b/Refresh.Database/Migrations/20260415112120_AddReasonAndTimestampToDisallowTables.cs @@ -0,0 +1,88 @@ +using System; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace Refresh.Database.Migrations +{ + /// + [DbContext(typeof(GameDatabaseContext))] + [Migration("20260415112120_AddReasonAndTimestampToDisallowTables")] + public partial class AddReasonAndTimestampToDisallowTables : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AddColumn( + name: "DisallowedAt", + table: "DisallowedUsers", + type: "timestamp with time zone", + nullable: false, + defaultValue: new DateTimeOffset(2026, 1, 1, 0, 0, 0, TimeSpan.Zero)); + + migrationBuilder.AddColumn( + name: "Reason", + table: "DisallowedUsers", + type: "text", + nullable: false, + defaultValue: ""); + + migrationBuilder.AddColumn( + name: "DisallowedAt", + table: "DisallowedEmailDomains", + type: "timestamp with time zone", + nullable: false, + defaultValue: new DateTimeOffset(2026, 1, 1, 0, 0, 0, TimeSpan.Zero)); + + migrationBuilder.AddColumn( + name: "Reason", + table: "DisallowedEmailDomains", + type: "text", + nullable: false, + defaultValue: ""); + + migrationBuilder.AddColumn( + name: "DisallowedAt", + table: "DisallowedEmailAddresses", + type: "timestamp with time zone", + nullable: false, + defaultValue: new DateTimeOffset(2026, 1, 1, 0, 0, 0, TimeSpan.Zero)); + + migrationBuilder.AddColumn( + name: "Reason", + table: "DisallowedEmailAddresses", + type: "text", + nullable: false, + defaultValue: ""); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropColumn( + name: "DisallowedAt", + table: "DisallowedUsers"); + + migrationBuilder.DropColumn( + name: "Reason", + table: "DisallowedUsers"); + + migrationBuilder.DropColumn( + name: "DisallowedAt", + table: "DisallowedEmailDomains"); + + migrationBuilder.DropColumn( + name: "Reason", + table: "DisallowedEmailDomains"); + + migrationBuilder.DropColumn( + name: "DisallowedAt", + table: "DisallowedEmailAddresses"); + + migrationBuilder.DropColumn( + name: "Reason", + table: "DisallowedEmailAddresses"); + } + } +} diff --git a/Refresh.Database/Migrations/GameDatabaseContextModelSnapshot.cs b/Refresh.Database/Migrations/GameDatabaseContextModelSnapshot.cs index 0eeb2f28..a1ea6665 100644 --- a/Refresh.Database/Migrations/GameDatabaseContextModelSnapshot.cs +++ b/Refresh.Database/Migrations/GameDatabaseContextModelSnapshot.cs @@ -1535,6 +1535,12 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.Property("Address") .HasColumnType("text"); + b.Property("DisallowedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Reason") + .HasColumnType("text"); + b.HasKey("Address"); b.ToTable("DisallowedEmailAddresses"); @@ -1545,6 +1551,12 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.Property("Domain") .HasColumnType("text"); + b.Property("DisallowedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Reason") + .HasColumnType("text"); + b.HasKey("Domain"); b.ToTable("DisallowedEmailDomains"); @@ -1555,6 +1567,12 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.Property("Username") .HasColumnType("text"); + b.Property("DisallowedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Reason") + .HasColumnType("text"); + b.HasKey("Username"); b.ToTable("DisallowedUsers"); diff --git a/Refresh.Database/Models/Users/DisallowedEmailAddress.cs b/Refresh.Database/Models/Users/DisallowedEmailAddress.cs index 2cbdb202..a54c75df 100644 --- a/Refresh.Database/Models/Users/DisallowedEmailAddress.cs +++ b/Refresh.Database/Models/Users/DisallowedEmailAddress.cs @@ -6,4 +6,6 @@ public partial class DisallowedEmailAddress { [Key] public string Address { get; set; } + public string Reason { get; set; } + public DateTimeOffset DisallowedAt { get; set; } } \ No newline at end of file diff --git a/Refresh.Database/Models/Users/DisallowedEmailDomain.cs b/Refresh.Database/Models/Users/DisallowedEmailDomain.cs index d0b69039..ae3e297d 100644 --- a/Refresh.Database/Models/Users/DisallowedEmailDomain.cs +++ b/Refresh.Database/Models/Users/DisallowedEmailDomain.cs @@ -6,4 +6,6 @@ public partial class DisallowedEmailDomain { [Key] public string Domain { get; set; } + public string Reason { get; set; } + public DateTimeOffset DisallowedAt { get; set; } } \ No newline at end of file diff --git a/Refresh.Database/Models/Users/DisallowedUser.cs b/Refresh.Database/Models/Users/DisallowedUser.cs index f26d0856..7dcd2beb 100644 --- a/Refresh.Database/Models/Users/DisallowedUser.cs +++ b/Refresh.Database/Models/Users/DisallowedUser.cs @@ -6,4 +6,6 @@ public partial class DisallowedUser { [Key] public string Username { get; set; } + public string Reason { get; set; } + public DateTimeOffset DisallowedAt { get; set; } } \ No newline at end of file diff --git a/Refresh.GameServer/CommandLineManager.cs b/Refresh.GameServer/CommandLineManager.cs index ed21a839..61c14a80 100644 --- a/Refresh.GameServer/CommandLineManager.cs +++ b/Refresh.GameServer/CommandLineManager.cs @@ -203,7 +203,7 @@ private void StartWithOptions(Options options) { if (options.Username != null) { - if (!this._server.DisallowUser(options.Username)) + if (!this._server.DisallowUser(options.Username, options.Reason)) Fail("User is already disallowed"); } else Fail("No username was provided"); @@ -221,7 +221,7 @@ private void StartWithOptions(Options options) { if (options.EmailAddress != null) { - if (!this._server.DisallowEmailAddress(options.EmailAddress)) + if (!this._server.DisallowEmailAddress(options.EmailAddress, options.Reason)) Fail("Email address is already disallowed"); } else Fail("No email address was provided"); @@ -239,7 +239,7 @@ private void StartWithOptions(Options options) { if (options.EmailAddress != null) { - if (!this._server.DisallowEmailDomain(options.EmailAddress)) + if (!this._server.DisallowEmailDomain(options.EmailAddress, options.Reason)) Fail("Email domain is already disallowed"); } else Fail("No email domain was provided"); diff --git a/Refresh.GameServer/RefreshGameServer.cs b/Refresh.GameServer/RefreshGameServer.cs index b8668714..92bb94dc 100644 --- a/Refresh.GameServer/RefreshGameServer.cs +++ b/Refresh.GameServer/RefreshGameServer.cs @@ -272,11 +272,12 @@ public void SetUserAsRole(GameUser user, GameUserRole role) context.SetUserRole(user, role); } - public bool DisallowUser(string username) + public bool DisallowUser(string username, string? reason) { using GameDatabaseContext context = this.GetContext(); - return context.DisallowUser(username); + (DisallowedUser disallowed, bool success) = context.DisallowUser(username, reason ?? ""); + return success; } public bool ReallowUser(string username) @@ -286,11 +287,12 @@ public bool ReallowUser(string username) return context.ReallowUser(username); } - public bool DisallowEmailAddress(string address) + public bool DisallowEmailAddress(string address, string? reason) { using GameDatabaseContext context = this.GetContext(); - return context.DisallowEmailAddress(address); + (DisallowedEmailAddress disallowed, bool success) = context.DisallowEmailAddress(address, reason ?? ""); + return success; } public bool ReallowEmailAddress(string address) @@ -300,11 +302,12 @@ public bool ReallowEmailAddress(string address) return context.ReallowEmailAddress(address); } - public bool DisallowEmailDomain(string domain) + public bool DisallowEmailDomain(string domain, string? reason) { using GameDatabaseContext context = this.GetContext(); - return context.DisallowEmailDomain(domain); + (DisallowedEmailDomain disallowed, bool success) = context.DisallowEmailDomain(domain, reason ?? ""); + return success; } public bool ReallowEmailDomain(string domain) diff --git a/RefreshTests.GameServer/Tests/ApiV3/UserApiTests.cs b/RefreshTests.GameServer/Tests/ApiV3/UserApiTests.cs index 8f4c2877..4c742708 100644 --- a/RefreshTests.GameServer/Tests/ApiV3/UserApiTests.cs +++ b/RefreshTests.GameServer/Tests/ApiV3/UserApiTests.cs @@ -54,7 +54,7 @@ public void CannotRegisterAccountWithDisallowedEmailAddress() const string email = "guy@lil.com"; // Not somehow already disallowed Assert.That(context.Database.IsEmailAddressDisallowed(email), Is.False); - context.Database.DisallowEmailAddress(email); + context.Database.DisallowEmailAddress(email, ""); context.Database.Refresh(); Assert.That(context.Database.IsEmailAddressDisallowed(email), Is.True); @@ -87,7 +87,7 @@ public void CannotRegisterAccountsWithDisallowedEmailDomain(string addressToBloc // Not somehow already disallowed Assert.That(context.Database.IsEmailDomainDisallowed(addressToBlockWith), Is.False); - context.Database.DisallowEmailDomain(addressToBlockWith); + context.Database.DisallowEmailDomain(addressToBlockWith, ""); context.Database.Refresh(); Assert.That(context.Database.IsEmailDomainDisallowed(addressToBlockWith), Is.True); @@ -161,7 +161,7 @@ public void CannotRegisterAccountWithDisallowedUsername() const string username = "a_lil_guy"; - context.Database.DisallowUser(username); + context.Database.DisallowUser(username, ""); ApiResponse? response = context.Http.PostData("/api/v3/register", new ApiRegisterRequest { From d1e375127644a81b416534aa5af748c063f6cdc4 Mon Sep 17 00:00:00 2001 From: Toaster2 Date: Thu, 16 Apr 2026 11:54:21 +0200 Subject: [PATCH 2/4] More thorough disallowance tests --- .../Tests/ApiV3/UserApiTests.cs | 45 ++++++++++++++++--- 1 file changed, 40 insertions(+), 5 deletions(-) diff --git a/RefreshTests.GameServer/Tests/ApiV3/UserApiTests.cs b/RefreshTests.GameServer/Tests/ApiV3/UserApiTests.cs index 4c742708..920df4ef 100644 --- a/RefreshTests.GameServer/Tests/ApiV3/UserApiTests.cs +++ b/RefreshTests.GameServer/Tests/ApiV3/UserApiTests.cs @@ -52,12 +52,18 @@ public void CannotRegisterAccountWithDisallowedEmailAddress() using TestContext context = this.GetServer(); const string email = "guy@lil.com"; + const string disallowReason = "being lil"; // Not somehow already disallowed Assert.That(context.Database.IsEmailAddressDisallowed(email), Is.False); - context.Database.DisallowEmailAddress(email, ""); + Assert.That(context.Database.GetDisallowedEmailAddressInfo(email), Is.Null); + context.Database.DisallowEmailAddress(email, disallowReason); context.Database.Refresh(); Assert.That(context.Database.IsEmailAddressDisallowed(email), Is.True); + DisallowedEmailAddress? disallowed = context.Database.GetDisallowedEmailAddressInfo(email); + Assert.That(disallowed, Is.Not.Null); + Assert.That(disallowed!.Address, Is.EqualTo(email)); + Assert.That(disallowed!.Reason, Is.EqualTo(disallowReason)); ApiResponse? response = context.Http.PostData("/api/v3/register", new ApiRegisterRequest { @@ -76,6 +82,7 @@ public void CannotRegisterAccountWithDisallowedEmailAddress() context.Database.ReallowEmailAddress(email); context.Database.Refresh(); Assert.That(context.Database.IsEmailAddressDisallowed(email), Is.False); + Assert.That(context.Database.GetDisallowedEmailAddressInfo(email), Is.Null); } [Test] @@ -84,13 +91,22 @@ public void CannotRegisterAccountWithDisallowedEmailAddress() public void CannotRegisterAccountsWithDisallowedEmailDomain(string addressToBlockWith) { using TestContext context = this.GetServer(); + const string disallowReason = "moron email moment"; + const string domain = "moron.com"; // Not somehow already disallowed Assert.That(context.Database.IsEmailDomainDisallowed(addressToBlockWith), Is.False); - context.Database.DisallowEmailDomain(addressToBlockWith, ""); + Assert.That(context.Database.IsEmailDomainDisallowed(domain), Is.False); + Assert.That(context.Database.GetDisallowedEmailDomainInfo(addressToBlockWith), Is.Null); + context.Database.DisallowEmailDomain(addressToBlockWith, disallowReason); context.Database.Refresh(); Assert.That(context.Database.IsEmailDomainDisallowed(addressToBlockWith), Is.True); + Assert.That(context.Database.IsEmailDomainDisallowed(domain), Is.True); + DisallowedEmailDomain? disallowed = context.Database.GetDisallowedEmailDomainInfo(addressToBlockWith); + Assert.That(disallowed, Is.Not.Null); + Assert.That(disallowed!.Domain, Is.EqualTo(domain)); + Assert.That(disallowed!.Reason, Is.EqualTo(disallowReason)); // Attempt 1 (block) ApiResponse? response = context.Http.PostData("/api/v3/register", new ApiRegisterRequest @@ -152,16 +168,29 @@ public void CannotRegisterAccountsWithDisallowedEmailDomain(string addressToBloc context.Database.ReallowEmailDomain(addressToBlockWith); context.Database.Refresh(); Assert.That(context.Database.IsEmailDomainDisallowed(addressToBlockWith), Is.False); + Assert.That(context.Database.IsEmailDomainDisallowed(domain), Is.False); + Assert.That(context.Database.GetDisallowedEmailDomainInfo(addressToBlockWith), Is.Null); } [Test] public void CannotRegisterAccountWithDisallowedUsername() { using TestContext context = this.GetServer(); - const string username = "a_lil_guy"; + const string disallowReason = "writing these is fun lol"; + + // Not somehow already disallowed + Assert.That(context.Database.IsUserDisallowed(username), Is.False); + Assert.That(context.Database.GetDisallowedUserInfo(username), Is.Null); + + context.Database.DisallowUser(username, disallowReason); + context.Database.Refresh(); - context.Database.DisallowUser(username, ""); + Assert.That(context.Database.IsUserDisallowed(username), Is.True); + DisallowedUser? disallowed = context.Database.GetDisallowedUserInfo(username); + Assert.That(disallowed, Is.Not.Null); + Assert.That(disallowed!.Username, Is.EqualTo(username)); + Assert.That(disallowed!.Reason, Is.EqualTo(disallowReason)); ApiResponse? response = context.Http.PostData("/api/v3/register", new ApiRegisterRequest { @@ -172,9 +201,15 @@ public void CannotRegisterAccountWithDisallowedUsername() Assert.That(response, Is.Not.Null); Assert.That(response!.Error, Is.Not.EqualTo(null)); Assert.That(response.Error!.Name, Is.EqualTo("ApiAuthenticationError")); + + context.Database.Refresh(); + Assert.That(context.Database.GetUserByUsername(username), Is.Null); + // Undo + context.Database.ReallowUser(username); context.Database.Refresh(); - Assert.That(context.Database.GetUserByUsername(username), Is.EqualTo(null)); + Assert.That(context.Database.IsUserDisallowed(username), Is.False); + Assert.That(context.Database.GetDisallowedUserInfo(username), Is.Null); } [Test] From 1be3ba9f9a614fea7d2ecd45116509c78a1359c6 Mon Sep 17 00:00:00 2001 From: Toaster2 Date: Thu, 16 Apr 2026 12:10:02 +0200 Subject: [PATCH 3/4] Shorter aliases for asset disallowance via CLI --- Refresh.GameServer/CommandLineManager.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Refresh.GameServer/CommandLineManager.cs b/Refresh.GameServer/CommandLineManager.cs index 61c14a80..2782b94d 100644 --- a/Refresh.GameServer/CommandLineManager.cs +++ b/Refresh.GameServer/CommandLineManager.cs @@ -80,13 +80,13 @@ private class Options [Option("reallow-asset", HelpText = "Re-allow an asset by hash. It may be uploaded and used in various UGC again. Asset option is required if this is set.")] public bool ReallowAsset { get; set; } - [Option("asset", HelpText = "The hash of the asset to operate on.")] + [Option('h', "asset", HelpText = "The hash of the asset to operate on.")] public string? AssetHash { get; set; } - [Option("type", HelpText = "The type of the asset to use. If this isn't set, we will use the corrensponding GameAsset's type from DB instead, if it exists.")] + [Option('t', "type", HelpText = "The type of the asset to use. If this isn't set, we will use the corresponding GameAsset's type from DB instead, if it exists.")] public string? AssetType { get; set; } - [Option("reason", HelpText = "The (usually optional) reason for a moderation action, such as asset disallowance.")] + [Option('r', "reason", HelpText = "The (usually optional) reason for a moderation action, such as asset disallowance.")] public string? Reason { get; set; } [Option("rename-user", HelpText = "Changes a user's username. (old) username or Email option is required if this is set.")] From f2757c815251606a7d461b1e574283fc308efecf Mon Sep 17 00:00:00 2001 From: Toaster2 Date: Thu, 16 Apr 2026 17:34:26 +0200 Subject: [PATCH 4/4] Order disallowed entities by recency, test disallowed asset filtering --- .../GameDatabaseContext.Assets.cs | 11 +++++---- .../GameDatabaseContext.Registration.cs | 6 ++--- .../Tests/Assets/AssetDisallowanceTests.cs | 23 +++++++++++++++++++ 3 files changed, 32 insertions(+), 8 deletions(-) diff --git a/Refresh.Database/GameDatabaseContext.Assets.cs b/Refresh.Database/GameDatabaseContext.Assets.cs index c6cb8257..7d2ab535 100644 --- a/Refresh.Database/GameDatabaseContext.Assets.cs +++ b/Refresh.Database/GameDatabaseContext.Assets.cs @@ -150,9 +150,10 @@ public IQueryable FilterOutAllowedAssets(List hashes) .Where(d => hashes.Contains(d.AssetHash)) .Select(d => d.AssetHash); - public DatabaseList GetDisallowedAssets(int skip, int count) - => new(this.DisallowedAssets, skip, count); - - public DatabaseList GetDisallowedAssetsByType(GameAssetType type, int skip, int count) - => new(this.DisallowedAssets.Where(d => d.AssetType == type), skip, count); + public DatabaseList GetDisallowedAssets(GameAssetType? type, int skip, int count) + { + IQueryable disallowedList = this.DisallowedAssets.OrderByDescending(d => d.DisallowedAt); + if (type != null) disallowedList = disallowedList.Where(d => d.AssetType == type); + return new(disallowedList, skip, count); + } } \ No newline at end of file diff --git a/Refresh.Database/GameDatabaseContext.Registration.cs b/Refresh.Database/GameDatabaseContext.Registration.cs index 713b55b9..215b8e23 100644 --- a/Refresh.Database/GameDatabaseContext.Registration.cs +++ b/Refresh.Database/GameDatabaseContext.Registration.cs @@ -225,7 +225,7 @@ public bool IsUserDisallowed(string username) => this.DisallowedUsers.FirstOrDefault(d => d.Username == username); public DatabaseList GetDisallowedUsers(int skip, int count) - => new(this.DisallowedUsers, skip, count); + => new(this.DisallowedUsers.OrderByDescending(d => d.DisallowedAt), skip, count); public (DisallowedUser, bool) DisallowUser(string username, string reason) { @@ -263,7 +263,7 @@ public bool IsEmailAddressDisallowed(string emailAddress) => this.DisallowedEmailAddresses.FirstOrDefault(d => d.Address == emailAddress); public DatabaseList GetDisallowedEmailAddresses(int skip, int count) - => new(this.DisallowedEmailAddresses, skip, count); + => new(this.DisallowedEmailAddresses.OrderByDescending(d => d.DisallowedAt), skip, count); public (DisallowedEmailAddress, bool) DisallowEmailAddress(string emailAddress, string reason) { @@ -310,7 +310,7 @@ public bool IsEmailDomainDisallowed(string emailAddress) } public DatabaseList GetDisallowedEmailDomains(int skip, int count) - => new(this.DisallowedEmailDomains, skip, count); + => new(this.DisallowedEmailDomains.OrderByDescending(d => d.DisallowedAt), skip, count); public (DisallowedEmailDomain, bool) DisallowEmailDomain(string emailAddress, string reason) { diff --git a/RefreshTests.GameServer/Tests/Assets/AssetDisallowanceTests.cs b/RefreshTests.GameServer/Tests/Assets/AssetDisallowanceTests.cs index c264eb15..bbeaba9b 100644 --- a/RefreshTests.GameServer/Tests/Assets/AssetDisallowanceTests.cs +++ b/RefreshTests.GameServer/Tests/Assets/AssetDisallowanceTests.cs @@ -1,4 +1,5 @@ using System.Security.Cryptography; +using Refresh.Database; using Refresh.Database.Models.Assets; using Refresh.Database.Models.Authentication; using Refresh.Database.Models.Users; @@ -158,4 +159,26 @@ public void CannotUploadDisallowedAssetFromApi() Assert.That(response?.Error, Is.Not.Null); Assert.That(response!.Error!.Name, Is.EqualTo(nameof(ApiModerationError))); } + + [Test] + public void FiltersDisallowedAssetListByType() + { + using TestContext context = this.GetServer(); + + context.Database.DisallowAsset("3", GameAssetType.Texture, ""); + context.Database.DisallowAsset("7", GameAssetType.Plan, ""); + context.Database.DisallowAsset("9", GameAssetType.Mesh, ""); + + Assert.That(context.Database.GetDisallowedAssetInfo("3"), Is.Not.Null); + Assert.That(context.Database.GetDisallowedAssetInfo("7"), Is.Not.Null); + Assert.That(context.Database.GetDisallowedAssetInfo("9"), Is.Not.Null); + + DatabaseList completeList = context.Database.GetDisallowedAssets(null, 0, 10); + Assert.That(completeList.TotalItems, Is.EqualTo(3)); + Assert.That(completeList.Items.Count(), Is.EqualTo(3)); + + DatabaseList filteredList = context.Database.GetDisallowedAssets(GameAssetType.Texture, 0, 10); + Assert.That(filteredList.TotalItems, Is.EqualTo(1)); + Assert.That(filteredList.Items.Count(), Is.EqualTo(1)); + } } \ No newline at end of file