fix(setup): stop the forced-onboarding gate trapping players who have no email

setup_required is what the SPA polls to decide whether the onboarding wall is
still owed, and it disagreed with the middleware that actually enforces the
wall. requireOnboarded lifts on a verified email OR an enrolled passkey;
setup_required answered `u.Email == "" || !hasPasskey`. A console-tier player
joins through the bind-code door with no email at all — by design, there is no
SMTP at that point — so the email term never clears and the SPA keeps them on
the setup screen forever, even after they enroll the passkey that already
unlocked the API for them.

The predicate now lives in one place (setupRequired) and both endpoints call
it, so the next edit to the unlock condition cannot drift them apart again.
Keying it on EmailVerified rather than email presence is the deliberate part:
presence is exactly the term that trapped the no-email player, and it was also
wrong on its own terms — an unverified address is not an authentication
factor, so it was never what the lockdown could safely lift on.

Also lands the regression test for the mechanism behind the live claim-403
report: /me/servers answers 200 for a bind-onboarded player (which is why the
dashboard renders the 认领 button at all) while claim, wake and status all
answer 403 with code "setup_required" — i.e. the refusal comes from
requireOnboarded before the handler, not from isOwnerOrAdmin inside it, which
would have said "forbidden". Enrolling a passkey and changing nothing else
lifts all three, which isolates the gate as the sole cause. The backend authz
is correct; the button that leads a locked-down player into a 403 is the
frontend's to hide.
This commit is contained in:
flyemoji committed 2026-07-22 14:40:28 +09:00
1 parent 60b0ec97ac
commit c4c964578e
4 files changed
+198 -10

No files matched your search

+12
View File
@@ -624,6 +624,18 @@ func (a *API) requireOnboarded(h http.HandlerFunc) http.HandlerFunc {
}
}
// setupRequired reports whether the caller still owes the forced-onboarding step,
// for the SPA to poll. It MUST stay in lockstep with requireOnboarded's unlock
// condition above: the lockdown lifts on a verified email OR an enrolled passkey.
// Keying on email PRESENCE instead of a passkey would trap a console-tier player —
// they join through the bind-code door with no email by design (no SMTP) and can
// only ever complete setup by enrolling a passkey, so any email term loops them
// forever. Passkey alone is the durable gate; email verification is a later,
// SMTP-dependent step.
func setupRequired(emailVerified, hasPasskey bool) bool {
return !emailVerified && !hasPasskey
}
// ---- request context plumbing ----
type ctxKey int