feat(auth)!: go fully passwordless and fix cross-check review findings

Remove password authentication everywhere; the only session doors are
passkey (WebAuthn), email OTP, in-game bind codes, QR scan-login, and
op-login vouching. Remediates the 33-finding cross-check review across
backend, CLI, panel, plugins, and docs.

Backend/CLI:
- Drop password routes and fields from account/user/onboard/auth
  handlers; align tests (new account subtests, naming reserves
  "console", op-login/onboard/qr-login test updates).
- Add migrations 0016_op_login.sql and 0017_drop_password.sql.
- Thread panel/admin hostnames from hostcfg through api.go,
  setup_panel.go, tui_root.go and tui_preflight.go instead of
  hardcoding; bootstrap.sh writes panel-hostname/admin-hostname
  into felis.toml.
- Reword breakglass and TUI copy for passwordless flows.

Panel:
- Delete the ChangePassword page and all password UI; align
  login/auth/api/types with the passwordless contract; add the
  migration and op-login approval flows.
- i18n: convert ImageBuildPage durations/status badges and
  ServerLuckPerms strings to translation keys; drop 72 orphan keys
  per locale; unify the title as "Felis - Console".

Plugins (all six rebuilt):
- Velocity waiting router returns 503 at_capacity during wake;
  MOTD/control-channel copy and config comments.
- Paper zh menu title; Limbo bind-code TTL 600s with panel_url
  preference; unified /link lines in fabric/forge/neoforge; shared
  link-client javadoc contract fixes.

Docs: openapi.yaml, sequence-diagrams.md, deploy/limbo/README.md and
plugins/README.md aligned with the implementation.

BREAKING CHANGE: migration 0017 irreversibly drops
users.password_hash and users.must_change_password; password login
cannot be restored after migrating.
This commit is contained in:
flyemoji committed 2026-07-20 04:47:32 +09:00
1 parent c96b36a41f
commit 7860152f57
97 files changed
+1923 -1444

No files matched your search

+2 -2
View File
@@ -103,7 +103,7 @@ func newOwnerModel(ctx context.Context, store ownerStore, osUser string, adminEx
// newOperatorModel builds the model for the Add-Operator break-glass operation. It
// always starts at admin authentication: adding an Operator presupposes an existing
// admin (that is why the menu only offers it when one exists), so there is no
// bootstrap branch and the password is always generated. The username is left empty
// bootstrap branch. The username is left empty
// on purpose — defaulting it to "owner" (as the Owner flow does) would make the
// happy path insert a duplicate and hit ErrConflict on every attempt.
func newOperatorModel(ctx context.Context, store ownerStore, osUser string) *ownerModel {
@@ -285,7 +285,7 @@ func (m *ownerModel) provisionCmd() tea.Cmd {
// performAddOperator and performBreakGlass share a signature; the operation
// discriminator selects which one runs. The operator path is insert-only and
// never flips local auth (see performAddOperator); the Owner path upserts and
// enables local-password login.
// enables local session sign-in.
perform := performBreakGlass
if m.operation == bgAddOperator {
perform = performAddOperator