feat(api): reclaim squatted usernames for Mojang-priority players (spec §B3)
When the configured third-party Yggdrasil and the official Mojang service
issue the same username under different UUIDs, the non-genuine squatter is
displaced in favour of the real Mojang owner (正版优先). This adds the
Go-verifiable data layer of that flow on the internal (velocity) face.
- migration 0006: username_blacklist (barred squatter UUIDs) and
player_data_holds (the displaced account's 30-day data stash), both keyed
by mc_uuid so the genuine Mojang player — identical username, different
UUID — is never caught by the bar.
- POST /api/v1/internal/player/reclaim bars the squatter UUID and stashes
its data in one transaction (all-or-nothing). It is idempotent on a
retried callback and returns the hold's effective expiry — the first
reclaim's window, never a fresh now()+30d — so the rejected player is told
the truth about how long their data is kept.
- GET /api/v1/internal/player/blacklist/{mc_uuid} is the login-gate check
velocity calls to reject a barred squatter before admitting them.
Scope: velocity collision-routing, the limbo prompt, the authlib
dual-backend and the data-inherit flow are code-only (Java plus a QR-bound
device session a row cannot express) and are not part of this slice. Unit
tests cover the handlers and the in-memory repo contract; the Postgres SQL
path is exercised by integration only.
This commit is contained in:
8 files changed
+611
No files matched your search
@@ -54,6 +54,24 @@ type fakeRepo struct {
|
||||
// player email OTPs (spec §B2). Keyed by row id; the verify path scans for the
|
||||
// newest live (user, purpose) just as the PG query does.
|
||||
otps map[string]*fakeEmailOTP
|
||||
// username-collision reclaim (spec §B3). blacklist mirrors username_blacklist
|
||||
// (mc_uuid -> barred), holds mirrors player_data_holds keyed by the held
|
||||
// (squatter) mc_uuid — both keyed by UUID, matching the PG UNIQUE(mc_uuid)
|
||||
// idempotency. They are written together by ReclaimUsername so the fake encodes
|
||||
// the same all-or-nothing contract the PG transaction enforces.
|
||||
blacklist map[string]bool
|
||||
holds map[string]fakeDataHold
|
||||
}
|
||||
|
||||
// fakeDataHold mirrors a player_data_holds row at the granularity the verifiable
|
||||
// (write-only) layer exercises: which name/data was stashed for the squatter UUID
|
||||
// and when the 30-day window ends. reclaimed_by_user_id/reclaimed_at have no fake
|
||||
// fields — the inherit flow that would set them is CODE-ONLY (deferred).
|
||||
type fakeDataHold struct {
|
||||
id string
|
||||
username string
|
||||
dataRef string
|
||||
expiresAt time.Time
|
||||
}
|
||||
|
||||
// fakeEmailOTP mirrors an email_otps row: only the code hash is held (never the
|
||||
@@ -107,6 +125,8 @@ func newFakeRepo() *fakeRepo {
|
||||
sessions: map[string]*fakeSession{},
|
||||
settings: map[string][]byte{},
|
||||
otps: map[string]*fakeEmailOTP{},
|
||||
blacklist: map[string]bool{},
|
||||
holds: map[string]fakeDataHold{},
|
||||
}
|
||||
}
|
||||
|
||||
@@ -210,6 +230,23 @@ func (f *fakeRepo) UserByMCUUID(_ context.Context, uuid string) (string, error)
|
||||
}
|
||||
return "", ErrNotFound
|
||||
}
|
||||
|
||||
// ReclaimUsername mirrors PGRepo.ReclaimUsername: it bars the squatter UUID and
|
||||
// stashes the data hold together (the all-or-nothing PG transaction), keyed by
|
||||
// mc_uuid so a repeat reclaim of an already-barred UUID is an idempotent no-op
|
||||
// (ON CONFLICT (mc_uuid) DO NOTHING on both tables) — the first reclaim wins and
|
||||
// a duplicate neither errors nor overwrites the stored hold.
|
||||
func (f *fakeRepo) ReclaimUsername(_ context.Context, id, squatterUUID, username, dataRef string, expiresAt time.Time) (time.Time, error) {
|
||||
if h, ok := f.holds[squatterUUID]; ok { // already stashed — idempotent no-op; keep & report the first window
|
||||
return h.expiresAt, nil
|
||||
}
|
||||
f.blacklist[squatterUUID] = true
|
||||
f.holds[squatterUUID] = fakeDataHold{id: id, username: username, dataRef: dataRef, expiresAt: expiresAt}
|
||||
return expiresAt, nil
|
||||
}
|
||||
func (f *fakeRepo) IsUsernameBlacklisted(_ context.Context, mcUUID string) (bool, error) {
|
||||
return f.blacklist[mcUUID], nil
|
||||
}
|
||||
func (f *fakeRepo) ClaimServer(_ context.Context, n, u string) (bool, error) {
|
||||
ok, present := f.claimOK[n]
|
||||
if !present {
|
||||
|
||||
Reference in new issue
Block a user