feat(auth): migrate console login to passwordless

Replace console password auth with a passwordless surface — the pre-session
login doors plus an identifier-first discovery endpoint — and remove the
password paths.

- Login doors (Public, pre-session): email-OTP, passkey assertion, op.console
  login with in-game approval, and setup-token redeem.
- /api/v1/auth/options: identifier-first discovery reporting which console
  methods an email can use. The single sanctioned existence oracle; methods
  are computed with no role branch, so staff and player accounts in the same
  credential state return byte-identical bodies (staffness invisible by
  construction).
- Remove password auth: drop StaffUser.PasswordHash and the /auth/login,
  /auth/change-password and /users/{id}/reset-password endpoints (and test).
- Data layer: UserByEmail, verified-email uniqueness, setup-token store
  (migration 0012).
- Reconcile docs/openapi.yaml with the served surface; the method/path/face/
  tier parity gate (TestOpenAPIMatchesServedRoutes) passes.
- felis TUI: in-game MC bind, owner/break-glass OP provisioning, version.
- Velocity /felis command suite.

Consolidates the accumulated backend migration work; the frontend (panel/)
is left untouched. Full Go tree green on WSL (go build ./... && go test ./...).
This commit is contained in:
flyemoji committed 2026-07-04 21:47:12 +09:00
1 parent 627883e89a
commit 0c1cc598c1
46 files changed
+5554 -1651

No files matched your search

+6 -10
View File
@@ -180,16 +180,12 @@ func (a *API) handleMe(w http.ResponseWriter, r *http.Request) {
emailVerified = u.EmailVerified
}
writeJSON(w, http.StatusOK, map[string]any{
"user_id": p.UserID,
"email": p.Email,
"role": p.Role,
"is_admin": p.IsAdmin(),
"is_owner": p.IsOwner(),
"email_verified": emailVerified,
// must_change_password is meaningful only on the local-password path; the JWT
// path leaves it false. The panel uses it to route a freshly-provisioned staff
// account straight to the change-password card before any other surface.
"must_change_password": p.MustChangePassword,
"user_id": p.UserID,
"email": p.Email,
"role": p.Role,
"is_admin": p.IsAdmin(),
"is_owner": p.IsOwner(),
"email_verified": emailVerified,
})
}