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:
97 files changed
+1923
-1444
No files matched your search
+27
-5
@@ -100,6 +100,12 @@ type API struct {
|
||||
// console (console.<root_domain>) the gate is inert.
|
||||
AdminHostname string
|
||||
|
||||
// PanelHostname is the player console host (console.<root_domain>) from
|
||||
// config. Used to render user-facing panel URLs (the /link code's panel_url
|
||||
// hint); empty falls back to console.<RootDomain> (see panelURL), mirroring
|
||||
// AdminHostname's fallback.
|
||||
PanelHostname string
|
||||
|
||||
// WakeCooldown throttles repeated wakes per server (spec §9.1: cooldown hangs
|
||||
// on the wake lever). Zero disables throttling.
|
||||
WakeCooldown time.Duration
|
||||
@@ -143,6 +149,21 @@ type API struct {
|
||||
streamCap *streamLimiter
|
||||
}
|
||||
|
||||
// panelURL returns the public player-console origin ("https://console.<root>"),
|
||||
// preferring the configured PanelHostname and falling back to the conventional
|
||||
// console.<RootDomain> label — the same convention hostIsAdminConsole applies
|
||||
// to the operator host. Empty when neither is configured (a bare test API).
|
||||
func (a *API) panelURL() string {
|
||||
host := a.PanelHostname
|
||||
if host == "" && a.RootDomain != "" {
|
||||
host = "console." + a.RootDomain
|
||||
}
|
||||
if host == "" {
|
||||
return ""
|
||||
}
|
||||
return "https://" + host
|
||||
}
|
||||
|
||||
// now returns the current time using the injected clock.
|
||||
func (a *API) now() time.Time {
|
||||
if a.Now != nil {
|
||||
@@ -237,7 +258,6 @@ func (a *API) internalAPIRoutes() []apiRoute {
|
||||
{Method: "GET", Pattern: "/readyz", Public: true, h: a.handleReadyz},
|
||||
|
||||
{Method: "GET", Pattern: "/api/v1/servers", h: a.handleListServers},
|
||||
{Method: "GET", Pattern: "/api/v1/servers/by-host/{host}", h: a.handleByHost},
|
||||
{Method: "POST", Pattern: "/api/v1/internal/servers/{name}/ready", h: a.handleReady},
|
||||
{Method: "POST", Pattern: "/api/v1/internal/servers/{name}/join-event", h: a.handleJoinEvent},
|
||||
// Domain-autostart (spec §9.1, §14): velocity drives the wake lever and polls
|
||||
@@ -282,10 +302,11 @@ func (a *API) internalAPIRoutes() []apiRoute {
|
||||
// (Mojang-first) and rewrites third-party UUIDs into a per-source namespace
|
||||
// before returning the canonical profile (handlers_hasjoined.go).
|
||||
{Method: "GET", Pattern: "/session/minecraft/hasJoined", Public: true, h: a.handleHasJoined},
|
||||
// Op-login (passwordless console login): an in-game op requests a login that
|
||||
// the web owner/admin approves, then redeems for a session. Internal face
|
||||
// carries the pending queue and the approve action (service-token auth, no
|
||||
// Principal); the external face carries the start/status/finish the op drives.
|
||||
// Op-login (passwordless op.console login): a staff member starts the login
|
||||
// on the web, and an ONLINE in-game admin vouches for it via velocity's
|
||||
// /felis web op approve. Internal face carries the pending queue and the
|
||||
// approve action (service-token auth, no Principal); the public face carries
|
||||
// the start/status/finish the staff member's browser drives.
|
||||
{Method: "GET", Pattern: "/api/v1/internal/op-login/pending", h: a.handleOpLoginPending},
|
||||
{Method: "POST", Pattern: "/api/v1/internal/op-login/{id}/approve", h: a.handleOpLoginApprove},
|
||||
|
||||
@@ -362,6 +383,7 @@ func (a *API) externalAPIRoutes() []apiRoute {
|
||||
{Method: "GET", Pattern: "/api/v1/servers/{name}/access/ban", h: a.handleAccessBanList},
|
||||
{Method: "POST", Pattern: "/api/v1/servers/{name}/access/permission", h: a.handleAccessPermission},
|
||||
{Method: "POST", Pattern: "/api/v1/servers/{name}/access/group", h: a.handleAccessGroup},
|
||||
{Method: "GET", Pattern: "/api/v1/servers/{name}/access/luckperms/{player}", h: a.handleAccessLuckPermsInfo},
|
||||
{Method: "GET", Pattern: "/api/v1/servers/{name}/status", h: a.handleStatus},
|
||||
// Identity self-read (spec §14 tiering): the panel reads this once at boot to
|
||||
// learn its own tier and decide which navigation surfaces to render. App-tier —
|
||||
|
||||
Reference in new issue
Block a user