feat(api): add public Bind-Code onboarding for the player console
Adds POST /api/v1/auth/bind, the one public pre-account entrypoint of the player console (console.<root_domain>). An account-less player redeems the one-time Bind Code minted in the in-game Login Lobby; in a single step the platform creates a role=user player, links it to the verified in-game UUID, and mints a host-only felis_session. Login is thus not forced at the edge while operations stay app-authenticated. The operator console (op.console.<root_domain>) is unaffected and stays behind Zero Trust: a code whose UUID resolves to a staff (role=admin) account is refused with 403 (ErrPlayerBindForbidden) without consuming the code, so the public door provably never yields an admin principal — the session it mints carries ViaAdminAccess=false and is host-only to console, never sent to op.console. Repo layer: new RedeemPlayerBindCode on the Repo interface, implemented on PGRepo (single tx: resolve code, create-or-fetch the player, consume) and the test fake. The returning-player branch is idempotent and is a deliberate standing "log in via the game" door, not just first-time onboarding. Honest labeling: - ORACLE-VERIFIED (Go): account/session logic — role=user, refuse-staff, idempotent create-or-fetch, single-use code, and the op.console redline (player session rejected on admin routes). Covered by handlers_onboard_test and the OpenAPI parity gate. - INTEGRATION-dependent: the endpoint's security rests on the Bind Code having been minted against an online-mode-Yggdrasil-authenticated UUID, a precondition that lives in velocity/Java and is not verifiable from this repo (CODE-ONLY). The Go layer proves the logic, not that identity guarantee. - No app-level attempt cap: rate-limiting is deferred to the edge as for the public /auth/login; the ~1e12 keyspace, single use and short TTL make a blind app-level cap non-critical.
This commit is contained in:
8 files changed
+585
No files matched your search
@@ -149,6 +149,29 @@ type Repo interface {
|
||||
// re-verifying the same (user, uuid) pair is idempotent and refreshes the stored
|
||||
// authSource. now is the API clock so expiry is testable.
|
||||
VerifyLinkCode(ctx context.Context, userID, code string, now time.Time) (mcUUID, authSource string, err error)
|
||||
// RedeemPlayerBindCode is the account-less player-console bootstrap (console-tier
|
||||
// access model): it redeems a one-time Bind Code into a PLAYER account + link in
|
||||
// one atomic step, so a first-time player with no Felis account can create one
|
||||
// from the console.<root_domain> door. Unlike VerifyLinkCode it takes NO prior
|
||||
// user — it creates or fetches one, keyed on the verified mc_uuid the code carries:
|
||||
//
|
||||
// - code missing/expired → ErrLinkCodeInvalid (does not consume it);
|
||||
// - the uuid is not yet linked → create a role='user' player row with id
|
||||
// newUserID (NULL password_hash, username derived from the uuid so it is unique
|
||||
// and deterministic), write the account_links binding, consume the code, and
|
||||
// return newUserID;
|
||||
// - the uuid is already linked to a role='user' player → return THAT user
|
||||
// (idempotent "log in via the game"), consuming the code;
|
||||
// - the uuid is linked to a role='admin' STAFF account → ErrPlayerBindForbidden
|
||||
// WITHOUT consuming the code (operators use op.console behind Zero Trust; the
|
||||
// public bootstrap never mints a session for an admin identity).
|
||||
//
|
||||
// Safe as an unauthenticated entrypoint because a Bind Code is minted internal-face
|
||||
// only (CreateLinkCode), against an online-mode-verified UUID, short-TTL and
|
||||
// single-use — possession already proves control of a Minecraft identity. now is
|
||||
// the API clock so expiry is testable. It returns the effective userID plus the
|
||||
// bound mc_uuid and authSource (for the response + audit).
|
||||
RedeemPlayerBindCode(ctx context.Context, newUserID, code string, now time.Time) (userID, mcUUID, authSource string, err error)
|
||||
// QuotaAvailable reports whether the user is under their max_servers quota
|
||||
// (spec §9.3 step ②, evaluated before provisioning).
|
||||
QuotaAvailable(ctx context.Context, userID string) (bool, error)
|
||||
|
||||
Reference in new issue
Block a user