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
@@ -14,22 +14,19 @@ import java.util.UUID;
|
||||
/**
|
||||
* FelisApiClient is the proxy's read/drive client for the felis-api internal face
|
||||
* (spec §7, §9). Where {@link LinkClient} mints account-link codes, this client
|
||||
* drives domain-autostart routing: it lists the registrable servers, resolves a
|
||||
* connecting virtual host to its server, polls a server's lifecycle status, pulls
|
||||
* the wake lever, and reports real player joins. It shares the {@link LinkConfig}
|
||||
* drives domain-autostart routing: it lists the registrable servers, polls a
|
||||
* server's lifecycle status, pulls the wake lever, and reports real player
|
||||
* joins. It shares the {@link LinkConfig}
|
||||
* (same internal base URL + service token) and the same zero-dependency JDK HTTP
|
||||
* stack, so it compiles straight into each loader jar with nothing to shade.
|
||||
*
|
||||
* <p>Every call authenticates with {@code Authorization: Bearer <serviceToken>}
|
||||
* and surfaces a non-success status as a {@link LinkException} carrying the HTTP
|
||||
* status, so the proxy can branch on it without parsing human text. The two that
|
||||
* matter for routing:
|
||||
* <ul>
|
||||
* <li>{@code wake} → 403 means the autostartPolicy gate refused this UUID (do
|
||||
* not enqueue the player); 429 means a wake is already cooling down
|
||||
* ("already waking, keep waiting"), not a failure.</li>
|
||||
* <li>{@code serverByHost} → 404 means the host maps to no server.</li>
|
||||
* </ul>
|
||||
* status, so the proxy can branch on it without parsing human text. The one that
|
||||
* matters most for routing: {@code wake} → 403 means the autostartPolicy gate
|
||||
* refused this UUID (do not enqueue the player); 429 means a wake is already
|
||||
* cooling down ("already waking, keep waiting"); 503 means the cluster is at
|
||||
* capacity (tell the player to try later — nothing is coming up).
|
||||
*/
|
||||
public final class FelisApiClient {
|
||||
private final LinkConfig config;
|
||||
@@ -57,16 +54,6 @@ public final class FelisApiClient {
|
||||
return out;
|
||||
}
|
||||
|
||||
/**
|
||||
* serverByHost resolves {@code subdomain.<root_domain>} to its server view
|
||||
* (GET /servers/by-host/{host}). A 404 surfaces as a LinkException with
|
||||
* statusCode 404 so the caller can distinguish "unknown host" from a transport
|
||||
* fault.
|
||||
*/
|
||||
public ServerView serverByHost(String host) throws LinkException {
|
||||
return ServerView.fromJson(getObject("/api/v1/servers/by-host/" + Objects.requireNonNull(host, "host"), 200));
|
||||
}
|
||||
|
||||
/** serverStatus reads one server's current lifecycle view (internal status). */
|
||||
public ServerView serverStatus(String name) throws LinkException {
|
||||
return ServerView.fromJson(getObject("/api/v1/internal/servers/" + Objects.requireNonNull(name, "name") + "/status", 200));
|
||||
@@ -75,8 +62,9 @@ public final class FelisApiClient {
|
||||
/**
|
||||
* wake pulls the domain-autostart lever for {@code name} on behalf of the
|
||||
* joining player (spec §9.1, §14). The reply (202) carries the current phase
|
||||
* and ready flag so the caller can decide whether to wait. A 403 (policy gate)
|
||||
* or 429 (cooldown) arrives as a LinkException the caller branches on.
|
||||
* and ready flag so the caller can decide whether to wait. A 403 (policy gate),
|
||||
* 429 (cooldown), or 503 {@code at_capacity} (running cap) arrives as a
|
||||
* LinkException the caller branches on.
|
||||
*/
|
||||
public ServerView wake(String name, UUID mcUuid) throws LinkException {
|
||||
Objects.requireNonNull(name, "name");
|
||||
@@ -142,7 +130,7 @@ public final class FelisApiClient {
|
||||
* completion leg of the in-game login flow (spec §B3). After the player redeems
|
||||
* the Bind Code on {@code console.<root_domain>} the login limbo polls this until
|
||||
* it flips true, then admits/transfers the player. {@code GET
|
||||
* /api/v1/internal/account/link/status/{mc_uuid}} → {@code {"linked":bool,...}};
|
||||
* /api/v1/internal/account/link/status/{mc_uuid}} → {@code {"linked":bool}};
|
||||
* read-only and keyed by the verified UUID, so it consumes nothing and is safe to
|
||||
* poll repeatedly. Anything but {@code linked:true} (including a missing field) is
|
||||
* reported as not-yet-linked — the caller keeps waiting rather than admitting on
|
||||
|
||||
@@ -22,7 +22,8 @@ import java.util.UUID;
|
||||
* <li>header {@code Authorization: Bearer <serviceToken>} (constant-time
|
||||
* compared server-side; an empty token fails closed)</li>
|
||||
* <li>request body {@code {"mc_uuid":"<uuid>"}}</li>
|
||||
* <li>success: HTTP 201 with {@code {"code","expires_at"}}</li>
|
||||
* <li>success: HTTP 201 with {@code {"code","expires_at","panel_url"?}}
|
||||
* ({@code panel_url} present only when a panel hostname is configured)</li>
|
||||
* <li>failure: the {@code {"error":{"code","message"}}} envelope</li>
|
||||
* </ul>
|
||||
*
|
||||
@@ -94,7 +95,11 @@ public final class LinkClient {
|
||||
"success body missing 'code'");
|
||||
}
|
||||
Object exp = obj.get("expires_at");
|
||||
return new LinkCode((String) code, exp instanceof String ? (String) exp : null);
|
||||
Object panelUrl = obj.get("panel_url");
|
||||
return new LinkCode((String) code,
|
||||
exp instanceof String ? (String) exp : null,
|
||||
panelUrl instanceof String && !((String) panelUrl).isEmpty()
|
||||
? (String) panelUrl : null);
|
||||
}
|
||||
|
||||
private LinkException parseError(int status, String text) {
|
||||
|
||||
@@ -13,10 +13,12 @@ import java.util.Objects;
|
||||
public final class LinkCode {
|
||||
private final String code;
|
||||
private final String expiresAt;
|
||||
private final String panelUrl;
|
||||
|
||||
public LinkCode(String code, String expiresAt) {
|
||||
public LinkCode(String code, String expiresAt, String panelUrl) {
|
||||
this.code = Objects.requireNonNull(code, "code");
|
||||
this.expiresAt = expiresAt;
|
||||
this.panelUrl = panelUrl;
|
||||
}
|
||||
|
||||
public String code() {
|
||||
@@ -27,4 +29,12 @@ public final class LinkCode {
|
||||
public String expiresAt() {
|
||||
return expiresAt;
|
||||
}
|
||||
|
||||
/**
|
||||
* panelUrl is the ready-to-open web-panel URL the server minted alongside the
|
||||
* code, or null when no panel hostname is configured server-side.
|
||||
*/
|
||||
public String panelUrl() {
|
||||
return panelUrl;
|
||||
}
|
||||
}
|
||||
@@ -5,8 +5,7 @@ import java.util.Map;
|
||||
/**
|
||||
* ServerView is the proxy-side mirror of the felis-api lifecycle view of one
|
||||
* MinecraftServer (the {@code ServerInfo} the internal face emits for
|
||||
* {@code GET /servers}, {@code GET /servers/by-host/{host}} and the internal
|
||||
* status/wake replies). It is an immutable, dependency-free value object so the
|
||||
* {@code GET /servers} and the internal status/wake replies). It is an immutable, dependency-free value object so the
|
||||
* shared link core stays zero-dependency and source-shareable across all four
|
||||
* loaders.
|
||||
*
|
||||
|
||||
Reference in new issue
Block a user