mirror of
https://github.com/DorkSquadRR/DorkNet.git
synced 2026-10-26 15:38:45 -07:00
4b5086cdf1
Sixteen agents, one controller each, over every route the coverage test reported as missing or wrong-verb. Route+verb coverage is now 516/522 (98.9%), up from 90.8%. Each handler was written against binary evidence rather than the audit's prose: verb from the BestHTTP.HTTPMethods ordinal at the call site, response shape from the issuing method's return type, request shape from whether the client uses form fields, a raw JsonUtility body, or query params. Agents were told to skip rather than stub, and several did. New surface worth noting: - Link host: actionlink create/get/consume and datalink get/put, previously unrouted entirely. - Club roster reads: members, banned, requests, requests/search, members/search, each with the client's real sort/paging params and the wire row shape its reader expects (the banned row has no MembershipType field, so it isn't sent). - Home clubhouse set/clear, club disband (soft, via the existing State column), and DELETE mainimage as its own action rather than a verb alias — the client parses the details envelope from it, so it is not status-only. - Club chat threads, comment CRUD, keepsake collect/delete, progression-event xp boosts, image report and cheered/bulk, iOS StoreKit product reporting. The six routes still missing all need storage that does not exist yet (per-role club permissions, club additional-image slots, MinLevel, ClubChatEnabled). Those are left for a single schema change rather than faked. Also excludes DELETE /account/me from the endpoint contract probe. That route is now a real, correctly-guarded account purge, and the probe walks every route with one test player — so calling it destroyed the probe's own session and turned one real event into 49 cascading foreign-key and 401 failures.
154 lines
8.8 KiB
C#
154 lines
8.8 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));
|
|
|
|
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}";
|
|
}
|
|
}
|