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

+13 -10
View File
@@ -52,13 +52,13 @@ func connectMethodLabel(m connectMethod) string {
type preflightDoneMsg struct{}
type ownerResultMsg struct {
username string
displayPassword string
mode string
accountable string
auditWarning string
isOperator bool // true when an Operator was added rather than the Owner provisioned
err error
username string
setupTokenURL string
mode string
accountable string
auditWarning string
isOperator bool // true when an Operator was added rather than the Owner provisioned
err error
}
// connectResultMsg is emitted by every connection method (the chooser for
@@ -207,6 +207,9 @@ func (m *rootModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
return m.showStatus()
}
m.stage = stageOwner
if m.mode == consoleModeSetup {
return m.adopt(newMCBindModel(m.ctx, m.store, m.adminHost, m.osUser))
}
return m.adopt(newOwnerModel(m.ctx, m.store, m.osUser, false))
case menuChoiceMsg:
@@ -228,7 +231,7 @@ func (m *rootModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
m.result.provisioned = true
m.result.isOperator = msg.isOperator
m.result.username = msg.username
m.result.displayPassword = msg.displayPassword
m.result.setupTokenURL = msg.setupTokenURL
m.result.mode = msg.mode
m.result.accountable = msg.accountable
m.result.auditWarning = msg.auditWarning
@@ -363,7 +366,7 @@ func (m *rootModel) reviewBody(stage int) string {
if m.result.username != "" {
b.WriteString(tuiLabel.Render("username ") + m.result.username + "\n")
}
b.WriteString(tuiHint.Render("Created and recorded. The one-time password was shown on the Owner step."))
b.WriteString(tuiHint.Render("Created and recorded. The one-time setup URL was shown on the Owner step."))
case stageConnect:
b.WriteString(tuiOK.Render("✓ Connection") + "\n")
b.WriteString(tuiLabel.Render("method ") + connectMethodLabel(m.result.connectMethod) + "\n")
@@ -488,7 +491,7 @@ func (m *rootModel) showSummary() (tea.Model, tea.Cmd) {
return m.adopt(&summaryModel{
panelURL: m.result.panelURL,
ownerUsername: m.result.username,
ownerPassword: m.result.displayPassword,
setupTokenURL: m.result.setupTokenURL,
accessLabel: connectMethodLabel(m.result.connectMethod),
storageLabel: m.result.storageDetail,
routedHosts: routed,