fix(api): in-game identity resolution and link takeover ignore dead accounts

UserByMCUUID now resolves only live accounts: claim, menu, wake
authorization, op-login vouch and the QR link-status poll treat a
disabled or soft-deleted link holder exactly like an unlinked UUID
instead of a retired identity. VerifyLinkCode lets a soft-deleted
link be taken over by a fresh in-game code (the deleted account is
gone, e.g. a migrated source), while a disabled holder still 409s so
the lockout is not bypassable; failed attempts still do not consume
the code. Fake repo and pgint coverage pin both branches.
This commit is contained in:
Lemon-miaow committed 2026-09-23 05:40:29 +08:00
1 parent 3ffa3f5318
commit bb9798e32c
5 files changed
+182 -7

No files matched your search

+22 -2
View File
@@ -287,7 +287,13 @@ func (f *fakeRepo) VerifyLinkCode(_ context.Context, userID, code string, now ti
return "", "", ErrLinkCodeInvalid
}
if existing, ok := f.links[rec.mcUUID]; ok && existing != userID {
return "", "", ErrConflict // do not consume another user's pending code
// A soft-deleted link's identity is unclaimed: the fresh in-game code lets a
// live caller take it over (mirrors PGRepo). Disabled-but-not-deleted stays a
// conflict — takeover there would bypass the lockout. Neither arm consumes
// the code.
if !f.seededDeleted(existing) {
return "", "", ErrConflict
}
}
f.links[rec.mcUUID] = userID
f.linkAuthSource[rec.mcUUID] = rec.authSource // copy/refresh, mirrors DO UPDATE
@@ -626,7 +632,7 @@ func (f *fakeRepo) UUIDInAllowlist(_ context.Context, n, uuid string) (bool, err
return f.allowUUID[n][uuid], nil
}
func (f *fakeRepo) UserByMCUUID(_ context.Context, uuid string) (string, error) {
if u, ok := f.links[uuid]; ok {
if u, ok := f.links[uuid]; ok && !f.seededDead(u) {
return u, nil
}
return "", ErrNotFound
@@ -1177,6 +1183,20 @@ func (f *fakeRepo) seededDead(id string) bool {
return false
}
// seededDeleted is the narrower liveness query: soft-deleted only (a disabled
// account still holds its identity, mirroring VerifyLinkCode's takeover rule).
func (f *fakeRepo) seededDeleted(id string) bool {
if f.deletedIDs[id] {
return true
}
for _, su := range f.seededUsers {
if su.view.ID == id {
return su.detail.DeletedAt != nil
}
}
return false
}
func (f *fakeRepo) GetQuotas(_ context.Context, userID string) (*QuotaView, error) {
if !f.liveUserExists(userID) {
return nil, ErrNotFound