mirror of
https://github.com/DorkSquadRR/DorkNet.git
synced 2026-10-26 15:38:45 -07:00
cf3aa29ba2
Security:
- roomcode PUT now requires instance ownership (any player could plant
or relocate codes on foreign private instances)
- /platformid/{accountId} requires auth (anonymous Steam/Oculus id
enumeration oracle)
- request tracing no longer captures successful /thread bodies by
default (chat messages landed unredacted in logs); opt in via
Trace__SuccessBodyPaths
- anonymous action-link renewal clamped to 720h per consume
Data integrity:
- buyPurchaseReminder upserts its objective sentinel (second purchase
threw UNIQUE after the wallet was debited)
- progression reward gift + idempotency sentinel commit in one
transaction (crash between them double-minted the reward)
- ClubPermissionsGalleryAndModeration migration defaults corrected to
the entity contract (CommentsJson '[]', ClubChatEnabled true); new
one-shot ClubChatDefaultRepair migration fixes databases that
applied the wrong defaults; bootstrap repairs '' CommentsJson rows
- admin subroom-maxplayers audit entry commits with the mutation
- account purge covers CustomAvatarItems (ownership deleted, created
items reassigned to the system account)
- duplicate photo reports ignored while one is unresolved
Stability:
- bootstrap only swallows the expected duplicate-column error; locked
databases and schema failures now fail startup instead of the first
endpoint
- clone blob write failure degrades to an empty sub-room instead of a
500 after the clone row committed
- resumed photo-feed cursor stamped DateTimeKind.Utc (Npgsql rejects
Unspecified for timestamptz)
- unreadcounts queries per requested room instead of materialising the
whole comment keyspace
Tests:
- destroys-the-probe-session template guard is case-insensitive
74/74 tests pass.
167 lines
9.5 KiB
C#
167 lines
9.5 KiB
C#
using Microsoft.EntityFrameworkCore;
|
|
using DorkNet.Server.Data;
|
|
|
|
namespace DorkNet.Server.Services;
|
|
|
|
/// <summary>
|
|
/// The single account-erasure cascade used by every "remove this player"
|
|
/// path in the server: the admin SPA's
|
|
/// <c>DELETE api/admin/v1/players/{id}</c> and the 2023 client's
|
|
/// <c>DELETE account/me</c> ("delete local account").
|
|
///
|
|
/// Personal rows are deleted outright; authored durable content (rooms,
|
|
/// clubs, inventions, playlists, chat threads …) is reassigned to the
|
|
/// system account so other players' worlds don't lose their contents when
|
|
/// one author leaves. Kept as a static helper — not a DI service — so both
|
|
/// call sites share one implementation without either controller having to
|
|
/// grow a constructor dependency.
|
|
///
|
|
/// Callers are responsible for the surrounding transaction, for removing
|
|
/// the <c>Players</c> row itself, and for kicking the live session.
|
|
/// </summary>
|
|
public static class PlayerAccountPurge
|
|
{
|
|
/// <summary>Delete/reassign every row that references
|
|
/// <paramref name="playerId"/>. Returns a one-line audit summary.</summary>
|
|
public static async Task<string> PurgeAsync(
|
|
DorkNetDbContext db, long playerId, long systemPlayerId)
|
|
{
|
|
var now = DateTime.UtcNow;
|
|
var deleted = 0;
|
|
var reassigned = 0;
|
|
|
|
deleted += await db.Avatars.Where(x => x.PlayerId == playerId).ExecuteDeleteAsync();
|
|
deleted += await db.PlayerSettings.Where(x => x.PlayerId == playerId).ExecuteDeleteAsync();
|
|
deleted += await db.Relationships.Where(x => x.RequesterId == playerId || x.TargetId == playerId).ExecuteDeleteAsync();
|
|
deleted += await db.CurrencyBalances.Where(x => x.PlayerId == playerId).ExecuteDeleteAsync();
|
|
deleted += await db.PlayerInventory.Where(x => x.PlayerId == playerId).ExecuteDeleteAsync();
|
|
deleted += await db.PlayerDevices.Where(x => x.PlayerId == playerId).ExecuteDeleteAsync();
|
|
deleted += await db.NotificationPrefs.Where(x => x.PlayerId == playerId).ExecuteDeleteAsync();
|
|
deleted += await db.CohortAssignments.Where(x => x.PlayerId == playerId).ExecuteDeleteAsync();
|
|
deleted += await db.PushTokens.Where(x => x.PlayerId == playerId).ExecuteDeleteAsync();
|
|
deleted += await db.PlatformIgnores.Where(x => x.PlayerId == playerId).ExecuteDeleteAsync();
|
|
deleted += await db.Cards.Where(x => x.PlayerId == playerId).ExecuteDeleteAsync();
|
|
deleted += await db.ObjectiveProgress.Where(x => x.PlayerId == playerId).ExecuteDeleteAsync();
|
|
deleted += await db.GameRewardSelections.Where(x => x.PlayerId == playerId).ExecuteDeleteAsync();
|
|
deleted += await db.CouponRedemptions.Where(x => x.PlayerId == playerId).ExecuteDeleteAsync();
|
|
deleted += await db.LeaderboardStats.Where(x => x.PlayerId == playerId).ExecuteDeleteAsync();
|
|
deleted += await db.PlayerElo.Where(x => x.PlayerId == playerId).ExecuteDeleteAsync();
|
|
deleted += await db.RoyalePlayerProgress.Where(x => x.PlayerId == playerId).ExecuteDeleteAsync();
|
|
deleted += await db.RoyaleMatchPlayers.Where(x => x.PlayerId == playerId).ExecuteDeleteAsync();
|
|
|
|
deleted += await db.Messages.Where(x => x.SenderPlayerId == playerId || x.RecipientPlayerId == playerId).ExecuteDeleteAsync();
|
|
deleted += await db.ChatMessages.Where(x => x.SenderPlayerId == playerId).ExecuteDeleteAsync();
|
|
deleted += await db.ChatThreadMembers.Where(x => x.PlayerId == playerId).ExecuteDeleteAsync();
|
|
reassigned += await db.ChatThreads
|
|
.Where(x => x.CreatorPlayerId == playerId)
|
|
.ExecuteUpdateAsync(s => s.SetProperty(x => x.CreatorPlayerId, systemPlayerId));
|
|
|
|
deleted += await db.Cheers.Where(x => x.FromPlayerId == playerId || x.TargetPlayerId == playerId).ExecuteDeleteAsync();
|
|
deleted += await db.Reports.Where(x => x.ReporterPlayerId == playerId || x.TargetPlayerId == playerId).ExecuteDeleteAsync();
|
|
deleted += await db.BugReports.Where(x => x.ReporterPlayerId == playerId).ExecuteDeleteAsync();
|
|
deleted += await db.Subscriptions.Where(x => x.SubscriberPlayerId == playerId || x.TargetPlayerId == playerId).ExecuteDeleteAsync();
|
|
|
|
deleted += await db.ClubMemberships.Where(x => x.PlayerId == playerId).ExecuteDeleteAsync();
|
|
deleted += await db.ClubSubscriptions.Where(x => x.PlayerId == playerId).ExecuteDeleteAsync();
|
|
deleted += await db.ClubAnnouncementReads.Where(x => x.PlayerId == playerId).ExecuteDeleteAsync();
|
|
reassigned += await db.Clubs
|
|
.Where(x => x.CreatorPlayerId == playerId)
|
|
.ExecuteUpdateAsync(s => s
|
|
.SetProperty(x => x.CreatorPlayerId, systemPlayerId)
|
|
.SetProperty(x => x.UpdatedAt, now));
|
|
reassigned += await db.ClubAnnouncements
|
|
.Where(x => x.AuthorPlayerId == playerId)
|
|
.ExecuteUpdateAsync(s => s.SetProperty(x => x.AuthorPlayerId, systemPlayerId));
|
|
|
|
deleted += await db.RoomBookmarks.Where(x => x.PlayerId == playerId).ExecuteDeleteAsync();
|
|
deleted += await db.RoomVisits.Where(x => x.PlayerId == playerId).ExecuteDeleteAsync();
|
|
deleted += await db.RoomRoles.Where(x => x.PlayerId == playerId).ExecuteDeleteAsync();
|
|
deleted += await db.RoomBans.Where(x => x.BannedPlayerId == playerId).ExecuteDeleteAsync();
|
|
reassigned += await db.RoomBans
|
|
.Where(x => x.BannedByPlayerId == playerId)
|
|
.ExecuteUpdateAsync(s => s.SetProperty(x => x.BannedByPlayerId, systemPlayerId));
|
|
reassigned += await db.RoomRoles
|
|
.Where(x => x.GrantedByPlayerId == playerId)
|
|
.ExecuteUpdateAsync(s => s.SetProperty(x => x.GrantedByPlayerId, systemPlayerId));
|
|
reassigned += await db.Rooms
|
|
.Where(x => x.CreatorPlayerId == playerId)
|
|
.ExecuteUpdateAsync(s => s
|
|
.SetProperty(x => x.CreatorPlayerId, systemPlayerId)
|
|
.SetProperty(x => x.UpdatedAt, now));
|
|
reassigned += await db.RoomDataBlobs
|
|
.Where(x => x.UploadedByPlayerId == playerId)
|
|
.ExecuteUpdateAsync(s => s.SetProperty(x => x.UploadedByPlayerId, systemPlayerId));
|
|
reassigned += await db.RoomKeys
|
|
.Where(x => x.CreatorPlayerId == playerId)
|
|
.ExecuteUpdateAsync(s => s.SetProperty(x => x.CreatorPlayerId, systemPlayerId));
|
|
deleted += await db.RoomKeyPurchases.Where(x => x.PlayerId == playerId).ExecuteDeleteAsync();
|
|
|
|
deleted += await db.PlaylistInteractions.Where(x => x.PlayerId == playerId).ExecuteDeleteAsync();
|
|
reassigned += await db.Playlists
|
|
.Where(x => x.CreatorPlayerId == playerId)
|
|
.ExecuteUpdateAsync(s => s.SetProperty(x => x.CreatorPlayerId, systemPlayerId));
|
|
|
|
deleted += await db.PlayerEventResponses
|
|
.Where(x => x.PlayerId == playerId
|
|
|| db.PlayerEvents.Where(e => e.CreatorPlayerId == playerId).Select(e => e.Id).Contains(x.EventId))
|
|
.ExecuteDeleteAsync();
|
|
deleted += await db.PlayerEvents.Where(x => x.CreatorPlayerId == playerId).ExecuteDeleteAsync();
|
|
|
|
deleted += await db.PrivateInstanceInvitees.Where(x => x.PlayerId == playerId).ExecuteDeleteAsync();
|
|
var privateInstanceIds = await db.PrivateInstances
|
|
.Where(x => x.OwnerPlayerId == playerId)
|
|
.Select(x => x.Id)
|
|
.ToListAsync();
|
|
if (privateInstanceIds.Count > 0)
|
|
{
|
|
deleted += await db.PrivateInstanceInvitees
|
|
.Where(x => privateInstanceIds.Contains(x.PrivateInstanceId))
|
|
.ExecuteDeleteAsync();
|
|
deleted += await db.PrivateInstances
|
|
.Where(x => privateInstanceIds.Contains(x.Id))
|
|
.ExecuteDeleteAsync();
|
|
}
|
|
|
|
deleted += await db.GiftPackages
|
|
.Where(x => x.RecipientPlayerId == playerId)
|
|
.ExecuteDeleteAsync();
|
|
if (playerId <= int.MaxValue)
|
|
{
|
|
reassigned += await db.GiftPackages
|
|
.Where(x => x.FromPlayerId == (int)playerId)
|
|
.ExecuteUpdateAsync(s => s.SetProperty(x => x.FromPlayerId, (int?)null));
|
|
}
|
|
|
|
reassigned += await db.Inventions
|
|
.Where(x => x.CreatorPlayerId == playerId)
|
|
.ExecuteUpdateAsync(s => s
|
|
.SetProperty(x => x.CreatorPlayerId, systemPlayerId)
|
|
.SetProperty(x => x.UpdatedAt, now));
|
|
reassigned += await db.Photos
|
|
.Where(x => x.UploaderPlayerId == playerId)
|
|
.ExecuteUpdateAsync(s => s
|
|
.SetProperty(x => x.UploaderPlayerId, systemPlayerId)
|
|
.SetProperty(x => x.DeletedAt, now)
|
|
.SetProperty(x => x.IsPublic, false));
|
|
|
|
// Custom avatar items: the purged player's ownership rows go away;
|
|
// items they CREATED are reassigned to the system account like
|
|
// Inventions, because other players may own and be wearing them —
|
|
// deleting the item would break those avatars.
|
|
deleted += await db.CustomAvatarItemOwnership
|
|
.Where(x => x.PlayerId == playerId)
|
|
.ExecuteDeleteAsync();
|
|
reassigned += await db.CustomAvatarItems
|
|
.Where(x => x.CreatorPlayerId == playerId)
|
|
.ExecuteUpdateAsync(s => s
|
|
.SetProperty(x => x.CreatorPlayerId, systemPlayerId)
|
|
.SetProperty(x => x.UpdatedAt, now));
|
|
|
|
reassigned += await db.SignupCodes
|
|
.Where(x => x.RedeemedByPlayerId == playerId)
|
|
.ExecuteUpdateAsync(s => s.SetProperty(x => x.RedeemedByPlayerId, (long?)null));
|
|
|
|
return $"deleted_rows={deleted} reassigned_rows={reassigned}";
|
|
}
|
|
}
|