From e89510e6a8865d4298d42eb56669677eacfa7451 Mon Sep 17 00:00:00 2001 From: Toaster2 Date: Thu, 12 Mar 2026 21:19:44 +0100 Subject: [PATCH 1/2] Delete more UGC when fake-deleting user, dont reset email --- Refresh.Database/GameDatabaseContext.Users.cs | 50 ++++++++++++------- .../Models/Relations/LevelCommentRelation.cs | 6 ++- .../Relations/ProfileCommentRelation.cs | 6 ++- 3 files changed, 43 insertions(+), 19 deletions(-) diff --git a/Refresh.Database/GameDatabaseContext.Users.cs b/Refresh.Database/GameDatabaseContext.Users.cs index 5a2145cf..9985a96c 100644 --- a/Refresh.Database/GameDatabaseContext.Users.cs +++ b/Refresh.Database/GameDatabaseContext.Users.cs @@ -395,14 +395,21 @@ public void DeleteUser(GameUser user) user.LocationX = 0; user.LocationY = 0; user.Description = deletedReason; - user.EmailAddress = null; + //user.EmailAddress = null; // should not be reset for now, to prevent them from just registering with it again user.PasswordBcrypt = "deleted"; user.JoinDate = DateTimeOffset.MinValue; user.LastLoginDate = DateTimeOffset.MinValue; user.Lbp2PlanetsHash = "0"; user.Lbp3PlanetsHash = "0"; user.VitaPlanetsHash = "0"; + user.BetaPlanetsHash = "0"; user.IconHash = "0"; + user.VitaIconHash = "0"; + user.BetaIconHash = "0"; + user.PspIconHash = "0"; + user.YayFaceHash = "0"; + user.BooFaceHash = "0"; + user.MehFaceHash = "0"; user.AllowIpAuthentication = false; user.EmailAddressVerified = false; user.PsnAuthenticationAllowed = false; @@ -413,27 +420,36 @@ public void DeleteUser(GameUser user) subject.UserId = null; } - this.FavouriteLevelRelations.RemoveRange(r => r.User == user); - this.FavouriteUserRelations.RemoveRange(r => r.UserToFavourite == user); - this.FavouriteUserRelations.RemoveRange(r => r.UserFavouriting == user); - this.QueueLevelRelations.RemoveRange(r => r.User == user); - this.GamePhotos.RemoveRange(p => p.Publisher == user); - this.GameUserVerifiedIpRelations.RemoveRange(p => p.User == user); + this.FavouriteLevelRelations.RemoveRange(r => r.UserId == user.UserId); + this.FavouriteUserRelations.RemoveRange(r => r.UserToFavouriteId == user.UserId); + this.FavouriteUserRelations.RemoveRange(r => r.UserFavouritingId == user.UserId); + this.QueueLevelRelations.RemoveRange(r => r.UserId == user.UserId); + this.TagLevelRelations.RemoveRange(r => r.UserId == user.UserId); + this.GameUserVerifiedIpRelations.RemoveRange(p => p.UserId == user.UserId); + + this.GameNotifications.RemoveRange(s => s.UserId == user.UserId); + this.GamePhotos.RemoveRange(p => p.PublisherId == user.UserId); + this.Events.RemoveRange(e => e.UserId == user.UserId); + this.GameScores.RemoveRange(s => s.PublisherId == user.UserId); + this.GameChallengeScores.RemoveRange(s => s.PublisherUserId == user.UserId); + + this.GameLevelComments.RemoveRange(s => s.AuthorUserId == user.UserId); + this.LevelCommentRelations.RemoveRange(s => s.UserId == user.UserId); + this.GameProfileComments.RemoveRange(s => s.AuthorUserId == user.UserId); + this.ProfileCommentRelations.RemoveRange(s => s.UserId == user.UserId); + this.GameReviews.RemoveRange(s => s.PublisherUserId == user.UserId); + this.RateReviewRelations.RemoveRange(s => s.UserId == user.UserId); - foreach (GameScore score in this.GameScores.ToList()) - { - if (!score.PlayerIds.Contains(user.UserId)) continue; - this.GameScores.Remove(score); - } - - foreach (GameLevel level in this.GameLevels.Where(l => l.Publisher == user)) + this.PinProgressRelations.RemoveRange(s => s.PublisherId == user.UserId); + this.ProfilePinRelations.RemoveRange(s => s.PublisherId == user.UserId); + this.GamePlaylists.RemoveRange(s => s.PublisherId == user.UserId); + + foreach (GameLevel level in this.GameLevels.Where(l => l.PublisherUserId == user.UserId)) { level.Publisher = null; } - this.GameChallengeScores.RemoveRange(s => s.Publisher == user); - - foreach (GameChallenge challenge in this.GameChallenges.Where(c => c.Publisher == user)) + foreach (GameChallenge challenge in this.GameChallenges.Where(c => c.PublisherUserId == user.UserId)) { challenge.Publisher = null; } diff --git a/Refresh.Database/Models/Relations/LevelCommentRelation.cs b/Refresh.Database/Models/Relations/LevelCommentRelation.cs index 68ef487e..4e0c9b0f 100644 --- a/Refresh.Database/Models/Relations/LevelCommentRelation.cs +++ b/Refresh.Database/Models/Relations/LevelCommentRelation.cs @@ -9,7 +9,11 @@ namespace Refresh.Database.Models.Relations; public partial class LevelCommentRelation : ICommentRelation { [Key] public ObjectId CommentRelationId { get; set; } = ObjectId.GenerateNewId(); - [Required] public GameUser User { get; set; } + + [ForeignKey(nameof(UserId)), Required] + public GameUser User { get; set; } + [Required] public ObjectId UserId { get; set; } + [Required] public GameLevelComment Comment { get; set; } public RatingType RatingType { get; set; } public DateTimeOffset Timestamp { get; set; } diff --git a/Refresh.Database/Models/Relations/ProfileCommentRelation.cs b/Refresh.Database/Models/Relations/ProfileCommentRelation.cs index 0b5d1079..38d057f2 100644 --- a/Refresh.Database/Models/Relations/ProfileCommentRelation.cs +++ b/Refresh.Database/Models/Relations/ProfileCommentRelation.cs @@ -9,7 +9,11 @@ namespace Refresh.Database.Models.Relations; public partial class ProfileCommentRelation : ICommentRelation { [Key] public ObjectId CommentRelationId { get; set; } = ObjectId.GenerateNewId(); - [Required] public GameUser User { get; set; } + + [ForeignKey(nameof(UserId)), Required] + public GameUser User { get; set; } + [Required] public ObjectId UserId { get; set; } + [Required] public GameProfileComment Comment { get; set; } public RatingType RatingType { get; set; } public DateTimeOffset Timestamp { get; set; } From 1c83be6fc5584ce1b391bcf91a342ebdedf0947e Mon Sep 17 00:00:00 2001 From: Toaster2 Date: Thu, 12 Mar 2026 22:02:40 +0100 Subject: [PATCH 2/2] Do reset email when deleting user again, but also disallow email --- .../GameDatabaseContext.Registration.cs | 35 ++++++++++++++++++- Refresh.Database/GameDatabaseContext.Users.cs | 4 ++- Refresh.Database/GameDatabaseContext.cs | 1 + ...260312203825_AddAbilityToDisallowEmails.cs | 35 +++++++++++++++++++ .../GameDatabaseContextModelSnapshot.cs | 10 ++++++ .../Models/Users/DisallowedEmail.cs | 9 +++++ Refresh.GameServer/CommandLineManager.cs | 26 +++++++++++++- Refresh.GameServer/RefreshGameServer.cs | 14 ++++++++ .../Endpoints/AuthenticationApiEndpoints.cs | 2 +- .../Tests/ApiV3/UserApiTests.cs | 22 ++++++++++++ .../Tests/Users/UserActionTests.cs | 16 +++++++++ 11 files changed, 170 insertions(+), 4 deletions(-) create mode 100644 Refresh.Database/Migrations/20260312203825_AddAbilityToDisallowEmails.cs create mode 100644 Refresh.Database/Models/Users/DisallowedEmail.cs diff --git a/Refresh.Database/GameDatabaseContext.Registration.cs b/Refresh.Database/GameDatabaseContext.Registration.cs index c6c87283..ba44dfa2 100644 --- a/Refresh.Database/GameDatabaseContext.Registration.cs +++ b/Refresh.Database/GameDatabaseContext.Registration.cs @@ -100,6 +100,7 @@ public bool IsUsernameTaken(string username, GameUser? userToName = null) { if (this.GameUsers.Any(u => u.Username == username)) return true; if (this.QueuedRegistrations.Any(r => r.Username == username)) return true; + if (this.IsUserDisallowed(username)) return true; PreviousUsername? previous = this.PreviousUsernames.FirstOrDefault(p => p.Username == username); // no one has ever had this name before @@ -113,7 +114,8 @@ public bool IsUsernameTaken(string username, GameUser? userToName = null) public bool IsEmailTaken(string emailAddress) { return this.GameUsers.Any(u => u.EmailAddress == emailAddress) || - this.QueuedRegistrations.Any(r => r.EmailAddress == emailAddress); + this.QueuedRegistrations.Any(r => r.EmailAddress == emailAddress) || + this.IsEmailDisallowed(emailAddress); } public void AddRegistrationToQueue(string username, string emailAddress, string passwordBcrypt) @@ -250,4 +252,35 @@ public bool IsUserDisallowed(string username) { return this.DisallowedUsers.FirstOrDefault(u => u.Username == username) != null; } + + public bool DisallowEmail(string email) + { + if (this.IsEmailDisallowed(email)) + return false; + + this.DisallowedEmails.Add(new() + { + Email = email, + }); + this.SaveChanges(); + + return true; + } + + public bool ReallowEmail(string email) + { + DisallowedEmail? disallowedEmail = this.DisallowedEmails.FirstOrDefault(u => u.Email == email); + if (disallowedEmail == null) + return false; + + this.DisallowedEmails.Remove(disallowedEmail); + this.SaveChanges(); + + return true; + } + + public bool IsEmailDisallowed(string email) + { + return this.DisallowedEmails.Any(u => u.Email == email); + } } \ No newline at end of file diff --git a/Refresh.Database/GameDatabaseContext.Users.cs b/Refresh.Database/GameDatabaseContext.Users.cs index 9985a96c..ebf1e6b8 100644 --- a/Refresh.Database/GameDatabaseContext.Users.cs +++ b/Refresh.Database/GameDatabaseContext.Users.cs @@ -389,13 +389,15 @@ public void DeleteUser(GameUser user) this.BanUser(user, deletedReason, DateTimeOffset.MaxValue); this.RevokeAllTokensForUser(user); this.DeleteNotificationsByUser(user); + if (user.EmailAddress != null) + this.DisallowEmail(user.EmailAddress); this.Write(() => { user.LocationX = 0; user.LocationY = 0; user.Description = deletedReason; - //user.EmailAddress = null; // should not be reset for now, to prevent them from just registering with it again + user.EmailAddress = null; user.PasswordBcrypt = "deleted"; user.JoinDate = DateTimeOffset.MinValue; user.LastLoginDate = DateTimeOffset.MinValue; diff --git a/Refresh.Database/GameDatabaseContext.cs b/Refresh.Database/GameDatabaseContext.cs index 7129719d..2acf5b11 100644 --- a/Refresh.Database/GameDatabaseContext.cs +++ b/Refresh.Database/GameDatabaseContext.cs @@ -64,6 +64,7 @@ public partial class GameDatabaseContext : DbContext, IDatabaseContext internal DbSet AssetDependencyRelations { get; set; } internal DbSet GameReviews { get; set; } internal DbSet DisallowedUsers { get; set; } + internal DbSet DisallowedEmails { get; set; } internal DbSet RateReviewRelations { get; set; } internal DbSet TagLevelRelations { get; set; } internal DbSet GamePlaylists { get; set; } diff --git a/Refresh.Database/Migrations/20260312203825_AddAbilityToDisallowEmails.cs b/Refresh.Database/Migrations/20260312203825_AddAbilityToDisallowEmails.cs new file mode 100644 index 00000000..7b12f592 --- /dev/null +++ b/Refresh.Database/Migrations/20260312203825_AddAbilityToDisallowEmails.cs @@ -0,0 +1,35 @@ +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace Refresh.Database.Migrations +{ + /// + [DbContext(typeof(GameDatabaseContext))] + [Migration("20260312203825_AddAbilityToDisallowEmails")] + public partial class AddAbilityToDisallowEmails : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.CreateTable( + name: "DisallowedEmails", + columns: table => new + { + Email = table.Column(type: "text", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_DisallowedEmails", x => x.Email); + }); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: "DisallowedEmails"); + } + } +} diff --git a/Refresh.Database/Migrations/GameDatabaseContextModelSnapshot.cs b/Refresh.Database/Migrations/GameDatabaseContextModelSnapshot.cs index d59ff565..7d567b6c 100644 --- a/Refresh.Database/Migrations/GameDatabaseContextModelSnapshot.cs +++ b/Refresh.Database/Migrations/GameDatabaseContextModelSnapshot.cs @@ -1510,6 +1510,16 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.ToTable("RequestStatistics"); }); + modelBuilder.Entity("Refresh.Database.Models.Users.DisallowedEmail", b => + { + b.Property("Email") + .HasColumnType("text"); + + b.HasKey("Email"); + + b.ToTable("DisallowedEmails"); + }); + modelBuilder.Entity("Refresh.Database.Models.Users.DisallowedUser", b => { b.Property("Username") diff --git a/Refresh.Database/Models/Users/DisallowedEmail.cs b/Refresh.Database/Models/Users/DisallowedEmail.cs new file mode 100644 index 00000000..27889601 --- /dev/null +++ b/Refresh.Database/Models/Users/DisallowedEmail.cs @@ -0,0 +1,9 @@ +namespace Refresh.Database.Models.Users; + +#nullable disable + +public partial class DisallowedEmail +{ + [Key] + public string Email { get; set; } +} \ No newline at end of file diff --git a/Refresh.GameServer/CommandLineManager.cs b/Refresh.GameServer/CommandLineManager.cs index 342d95f2..d8737340 100644 --- a/Refresh.GameServer/CommandLineManager.cs +++ b/Refresh.GameServer/CommandLineManager.cs @@ -59,6 +59,12 @@ private class Options [Option("reallow-user", HelpText = "Re-allow a user to register. Username option is required if this is set.")] public bool ReallowUser { get; set; } + + [Option("disallow-email", HelpText = "Disallow the email from being used by anyone in the future. Email option is required if this is set.")] + public bool DisallowEmail { get; set; } + + [Option("reallow-email", HelpText = "Re-allow the email to be used by anyone. Email option is required if this is set")] + public bool ReallowEmail { get; set; } [Option("rename-user", HelpText = "Changes a user's username. (old) username or Email option is required if this is set.")] public string? RenameUser { get; set; } @@ -188,10 +194,28 @@ private void StartWithOptions(Options options) } else Fail("No username was provided"); } + else if (options.DisallowEmail) + { + if (options.EmailAddress != null) + { + if (!this._server.DisallowEmail(options.EmailAddress)) + Fail("Email address is already disallowed"); + } + else Fail("No email address was provided"); + } + else if (options.ReallowEmail) + { + if (options.EmailAddress != null) + { + if (!this._server.ReallowEmail(options.EmailAddress)) + Fail("Email address is already allowed"); + } + else Fail("No email address was provided"); + } else if (options.RenameUser != null) { if(string.IsNullOrWhiteSpace(options.RenameUser)) - Fail("Username must contain content"); + Fail("Email address must contain content"); GameUser user = this.GetUserOrFail(options); this._server.RenameUser(user, options.RenameUser, options.Force); diff --git a/Refresh.GameServer/RefreshGameServer.cs b/Refresh.GameServer/RefreshGameServer.cs index 21b211df..17d43273 100644 --- a/Refresh.GameServer/RefreshGameServer.cs +++ b/Refresh.GameServer/RefreshGameServer.cs @@ -285,6 +285,20 @@ public bool ReallowUser(string username) return context.ReallowUser(username); } + public bool DisallowEmail(string email) + { + using GameDatabaseContext context = this.GetContext(); + + return context.DisallowEmail(email); + } + + public bool ReallowEmail(string email) + { + using GameDatabaseContext context = this.GetContext(); + + return context.ReallowEmail(email); + } + public void RenameUser(GameUser user, string newUsername, bool force = false) { using GameDatabaseContext context = this.GetContext(); diff --git a/Refresh.Interfaces.APIv3/Endpoints/AuthenticationApiEndpoints.cs b/Refresh.Interfaces.APIv3/Endpoints/AuthenticationApiEndpoints.cs index d18de5c9..cc5eaa1e 100644 --- a/Refresh.Interfaces.APIv3/Endpoints/AuthenticationApiEndpoints.cs +++ b/Refresh.Interfaces.APIv3/Endpoints/AuthenticationApiEndpoints.cs @@ -324,7 +324,7 @@ public ApiResponse Register(RequestContext context, if (!smtpService.CheckEmailDomainValidity(body.EmailAddress)) return ApiValidationError.EmailDoesNotActuallyExistError; - if (database.IsUserDisallowed(body.Username)) + if (database.IsUserDisallowed(body.Username) || database.IsEmailDisallowed(body.EmailAddress)) return new ApiAuthenticationError("You aren't allowed to play on this instance."); if (!database.IsUsernameValid(body.Username)) diff --git a/RefreshTests.GameServer/Tests/ApiV3/UserApiTests.cs b/RefreshTests.GameServer/Tests/ApiV3/UserApiTests.cs index f1d13e93..fdcc370d 100644 --- a/RefreshTests.GameServer/Tests/ApiV3/UserApiTests.cs +++ b/RefreshTests.GameServer/Tests/ApiV3/UserApiTests.cs @@ -45,6 +45,28 @@ public void RegisterAccount() context.Database.Refresh(); Assert.That(context.Database.GetUserByUsername(username), Is.Not.EqualTo(null)); } + + [Test] + public void CannotRegisterAccountWithDisallowedEmail() + { + using TestContext context = this.GetServer(); + + const string email = "guy@lil.com"; + context.Database.DisallowEmail(email); + + ApiResponse? response = context.Http.PostData("/api/v3/register", new ApiRegisterRequest + { + Username = "a_lil_guy", + EmailAddress = email, + PasswordSha512 = "ee26b0dd4af7e749aa1a8ee3c10ae9923f618980772e473f8819a5d4940e0db27ac185f8a0e1d5f84f88bc887fd67b143732c304cc5fa9ad8e6f57f50028a8ff", + }, false, true); + Assert.That(response, Is.Not.Null); + Assert.That(response!.Error, Is.Not.Null); + Assert.That(response.Error!.Name, Is.EqualTo("ApiAuthenticationError")); + + context.Database.Refresh(); + Assert.That(context.Database.GetUserByEmailAddress(email), Is.Null); + } [Test] public void CannotRegisterAccountWithDisallowedUsername() diff --git a/RefreshTests.GameServer/Tests/Users/UserActionTests.cs b/RefreshTests.GameServer/Tests/Users/UserActionTests.cs index b5b4ba61..9d9fc12c 100644 --- a/RefreshTests.GameServer/Tests/Users/UserActionTests.cs +++ b/RefreshTests.GameServer/Tests/Users/UserActionTests.cs @@ -81,4 +81,20 @@ public void CanResetOwnIcon(string newIcon) Assert.That(userUpdated, Is.Not.Null); Assert.That(userUpdated!.IconHash, Is.EqualTo("0")); } + + [Test] + public void DeletingUserDisallowsEmail() + { + using TestContext context = this.GetServer(); + GameUser publisher = context.CreateUser(); + Assert.That(publisher.EmailAddress, Is.Not.Null); + Assert.That(context.Database.IsEmailDisallowed(publisher.EmailAddress!), Is.False); + string email = publisher.EmailAddress!; + + // Delete publisher + context.Database.DeleteUser(publisher); + context.Database.Refresh(); + + Assert.That(context.Database.IsEmailDisallowed(email), Is.True); + } } \ No newline at end of file