fix: clearer errors for custom server addresses

This commit is contained in:
Felitendo committed 2026-09-17 16:53:23 +02:00
1 parent da8048a2d4
commit e2c72bcff8
2 files changed
+73 -36

No files matched your search

@@ -25,7 +25,7 @@ sits in the refresh token column in front of the client token, and the
account list shows which server an account is on. account list shows which server an account is on.
--- ---
.../src/components/ui/AccountsCard.vue | 112 +++-- .../src/components/ui/AccountsCard.vue | 112 +++--
.../src/components/ui/AuthlibAccountModal.vue | 307 +++++++++++++ .../src/components/ui/AuthlibAccountModal.vue | 334 ++++++++++++++
.../src/components/ui/MicrosoftLogo.vue | 8 + .../src/components/ui/MicrosoftLogo.vue | 8 +
.../components/ui/TitleBarAccountSwitcher.vue | 63 ++- .../components/ui/TitleBarAccountSwitcher.vue | 63 ++-
.../MinecraftRequiredModal.vue | 23 +- .../MinecraftRequiredModal.vue | 23 +-
@@ -35,8 +35,8 @@ account list shows which server an account is on.
packages/app-lib/src/api/minecraft_auth.rs | 77 +++- packages/app-lib/src/api/minecraft_auth.rs | 77 +++-
packages/app-lib/src/launcher/mod.rs | 11 +- packages/app-lib/src/launcher/mod.rs | 11 +-
packages/app-lib/src/state/minecraft_auth.rs | 422 ++++++++++++------ packages/app-lib/src/state/minecraft_auth.rs | 422 ++++++++++++------
packages/app-lib/src/util/authlib_injector.rs | 134 +++++- packages/app-lib/src/util/authlib_injector.rs | 138 +++++-
12 files changed, 1001 insertions(+), 211 deletions(-) 12 files changed, 1032 insertions(+), 211 deletions(-)
create mode 100644 apps/app-frontend/src/components/ui/AuthlibAccountModal.vue create mode 100644 apps/app-frontend/src/components/ui/AuthlibAccountModal.vue
create mode 100644 apps/app-frontend/src/components/ui/MicrosoftLogo.vue create mode 100644 apps/app-frontend/src/components/ui/MicrosoftLogo.vue
@@ -260,10 +260,10 @@ index 632483e..03a61f2 100644
id: 'minecraft-account.remove-account', id: 'minecraft-account.remove-account',
diff --git a/apps/app-frontend/src/components/ui/AuthlibAccountModal.vue b/apps/app-frontend/src/components/ui/AuthlibAccountModal.vue diff --git a/apps/app-frontend/src/components/ui/AuthlibAccountModal.vue b/apps/app-frontend/src/components/ui/AuthlibAccountModal.vue
new file mode 100644 new file mode 100644
index 0000000..4aab3f9 index 0000000..a2873fe
--- /dev/null --- /dev/null
+++ b/apps/app-frontend/src/components/ui/AuthlibAccountModal.vue +++ b/apps/app-frontend/src/components/ui/AuthlibAccountModal.vue
@@ -0,0 +1,307 @@ @@ -0,0 +1,334 @@
+<template> +<template>
+ <NewModal ref="modal" :header="formatMessage(messages.header)" max-width="480px" width="100%"> + <NewModal ref="modal" :header="formatMessage(messages.header)" max-width="480px" width="100%">
+ <div class="flex flex-col gap-4"> + <div class="flex flex-col gap-4">
@@ -301,6 +301,13 @@ index 0000000..4aab3f9
+ {{ formatMessage(messages.register) }} + {{ formatMessage(messages.register) }}
+ </a> + </a>
+ </p> + </p>
+ <p
+ v-else-if="lookingUp"
+ class="m-0 flex items-center gap-2 text-sm leading-tight text-secondary"
+ >
+ <SpinnerIcon aria-hidden="true" class="animate-spin" />
+ {{ formatMessage(messages.lookingUp) }}
+ </p>
+ <p v-else class="m-0 text-sm leading-tight text-secondary"> + <p v-else class="m-0 text-sm leading-tight text-secondary">
+ {{ formatMessage(messages.serverHint) }} + {{ formatMessage(messages.serverHint) }}
+ </p> + </p>
@@ -437,10 +444,14 @@ index 0000000..4aab3f9
+const profile = ref<Profile | null>(null) +const profile = ref<Profile | null>(null)
+const error = ref('') +const error = ref('')
+const submitting = ref(false) +const submitting = ref(false)
+const lookingUp = ref(false)
+let lookup = 0 +let lookup = 0
+ +
+function message(e: unknown) { +function message(e: unknown) {
+ return typeof e === 'string' ? e : ((e as Error)?.message ?? formatMessage(messages.genericError)) + const text =
+ typeof e === 'string' ? e : ((e as Error)?.message ?? formatMessage(messages.genericError))
+ // The launcher's own label for the kind of error says nothing here.
+ return text.replace(/^(Error|Invalid input): /, '')
+} +}
+ +
+// The players belong to the account they were listed for. +// The players belong to the account they were listed for.
@@ -451,6 +462,7 @@ index 0000000..4aab3f9
+ +
+watch(address, () => { +watch(address, () => {
+ lookup++ + lookup++
+ lookingUp.value = false
+ server.value = null + server.value = null
+ serverError.value = '' + serverError.value = ''
+}) +})
@@ -477,11 +489,15 @@ index 0000000..4aab3f9
+ if (!value || server.value) return + if (!value || server.value) return
+ +
+ const request = ++lookup + const request = ++lookup
+ lookingUp.value = true
+ serverError.value = ''
+ try { + try {
+ const found = await authlib_server(value) + const found = await authlib_server(value)
+ if (request === lookup) server.value = found + if (request === lookup) server.value = found
+ } catch (e) { + } catch (e) {
+ if (request === lookup) serverError.value = message(e) + if (request === lookup) serverError.value = message(e)
+ } finally {
+ if (request === lookup) lookingUp.value = false
+ } + }
+} +}
+ +
@@ -495,6 +511,13 @@ index 0000000..4aab3f9
+ submitting.value = true + submitting.value = true
+ error.value = '' + error.value = ''
+ +
+ // A server that cannot be found is the address's fault, not the password's.
+ await lookUpServer()
+ if (!server.value) {
+ submitting.value = false
+ return
+ }
+
+ try { + try {
+ const result = await login_authlib( + const result = await login_authlib(
+ address.value.trim(), + address.value.trim(),
@@ -536,6 +559,10 @@ index 0000000..4aab3f9
+ id: 'app.authlib-account.server-hint', + id: 'app.authlib-account.server-hint',
+ defaultMessage: "The address of the server's website.", + defaultMessage: "The address of the server's website.",
+ }, + },
+ lookingUp: {
+ id: 'app.authlib-account.looking-up',
+ defaultMessage: 'Looking for the server…',
+ },
+ register: { + register: {
+ id: 'app.authlib-account.register', + id: 'app.authlib-account.register',
+ defaultMessage: 'Create an account', + defaultMessage: 'Create an account',
@@ -1540,10 +1567,10 @@ index 2514d8e..0245152 100644
} }
} }
diff --git a/packages/app-lib/src/util/authlib_injector.rs b/packages/app-lib/src/util/authlib_injector.rs diff --git a/packages/app-lib/src/util/authlib_injector.rs b/packages/app-lib/src/util/authlib_injector.rs
index dc099f9..709e6d0 100644 index dc099f9..13591ef 100644
--- a/packages/app-lib/src/util/authlib_injector.rs --- a/packages/app-lib/src/util/authlib_injector.rs
+++ b/packages/app-lib/src/util/authlib_injector.rs +++ b/packages/app-lib/src/util/authlib_injector.rs
@@ -1,16 +1,133 @@ @@ -1,16 +1,137 @@
-//! Downloads and caches the authlib-injector Java agent. -//! Downloads and caches the authlib-injector Java agent.
+//! The authlib-injector Java agent, and the servers it works with. +//! The authlib-injector Java agent, and the servers it works with.
//! //!
@@ -1630,16 +1657,20 @@ index dc099f9..709e6d0 100644
+ )) + ))
+ })?; + })?;
+ +
+ // What went wrong in detail is for the log: to the player, a mistyped
+ // address and a server that is down look the same.
+ async fn get(url: &Url) -> crate::Result<Response> { + async fn get(url: &Url) -> crate::Result<Response> {
+ let host = url.host_str().unwrap_or_default();
+ INSECURE_REQWEST_CLIENT + INSECURE_REQWEST_CLIENT
+ .get(url.clone()) + .get(url.clone())
+ .header(ACCEPT, "application/json") + .header(ACCEPT, "application/json")
+ .timeout(std::time::Duration::from_secs(10))
+ .send() + .send()
+ .await + .await
+ .map_err(|error| { + .map_err(|error| {
+ tracing::warn!("Could not reach {host}: {error:?}");
+ crate::ErrorKind::OtherError(format!( + crate::ErrorKind::OtherError(format!(
+ "Could not reach {}: {error}", + "Could not reach {host}. Check the address and your connection."
+ url.host_str().unwrap_or_default()
+ )) + ))
+ .as_error() + .as_error()
+ }) + })
@@ -1683,7 +1714,7 @@ index dc099f9..709e6d0 100644
/// The agent's own distribution metadata. /// The agent's own distribution metadata.
const LATEST_URL: &str = "https://authlib-injector.yushi.moe/artifact/latest.json"; const LATEST_URL: &str = "https://authlib-injector.yushi.moe/artifact/latest.json";
@@ -22,8 +139,7 @@ struct LatestArtifact { @@ -22,8 +143,7 @@ struct LatestArtifact {
/// Returns the path to the agent jar, downloading it once if it is not cached. /// Returns the path to the agent jar, downloading it once if it is not cached.
/// ///
/// The cached copy is reused as it is. The agent is not tied to a game or /// The cached copy is reused as it is. The agent is not tied to a game or
@@ -1693,7 +1724,7 @@ index dc099f9..709e6d0 100644
pub async fn get_authlib_injector( pub async fn get_authlib_injector(
directories: &DirectoryInfo, directories: &DirectoryInfo,
) -> crate::Result<PathBuf> { ) -> crate::Result<PathBuf> {
@@ -35,7 +151,7 @@ pub async fn get_authlib_injector( @@ -35,7 +155,7 @@ pub async fn get_authlib_injector(
return Ok(jar); return Ok(jar);
} }
@@ -29,11 +29,11 @@ session in the refresh token column as JSON behind a marker.
apps/app/src/api/auth.rs | 49 +++ apps/app/src/api/auth.rs | 49 +++
packages/app-lib/src/api/minecraft_auth.rs | 47 +- packages/app-lib/src/api/minecraft_auth.rs | 47 +-
packages/app-lib/src/state/minecraft_auth.rs | 402 +++++++++++++++++- packages/app-lib/src/state/minecraft_auth.rs | 402 +++++++++++++++++-
packages/app-lib/src/util/authlib_injector.rs | 94 ++++ packages/app-lib/src/util/authlib_injector.rs | 100 +++++
7 files changed, 782 insertions(+), 14 deletions(-) 7 files changed, 788 insertions(+), 14 deletions(-)
diff --git a/apps/app-frontend/src/components/ui/AuthlibAccountModal.vue b/apps/app-frontend/src/components/ui/AuthlibAccountModal.vue diff --git a/apps/app-frontend/src/components/ui/AuthlibAccountModal.vue b/apps/app-frontend/src/components/ui/AuthlibAccountModal.vue
index 4aab3f9..b67b2c7 100644 index a2873fe..0e1f9a7 100644
--- a/apps/app-frontend/src/components/ui/AuthlibAccountModal.vue --- a/apps/app-frontend/src/components/ui/AuthlibAccountModal.vue
+++ b/apps/app-frontend/src/components/ui/AuthlibAccountModal.vue +++ b/apps/app-frontend/src/components/ui/AuthlibAccountModal.vue
@@ -1,5 +1,11 @@ @@ -1,5 +1,11 @@
@@ -49,7 +49,7 @@ index 4aab3f9..b67b2c7 100644
<div class="flex flex-col gap-4"> <div class="flex flex-col gap-4">
<p class="m-0 leading-tight text-secondary"> <p class="m-0 leading-tight text-secondary">
{{ formatMessage(messages.description) }} {{ formatMessage(messages.description) }}
@@ -40,6 +46,52 @@ @@ -47,6 +53,52 @@
</p> </p>
</div> </div>
@@ -102,7 +102,7 @@ index 4aab3f9..b67b2c7 100644
<div class="flex flex-col gap-2"> <div class="flex flex-col gap-2">
<label class="font-semibold text-contrast" for="authlib-account-username"> <label class="font-semibold text-contrast" for="authlib-account-username">
{{ formatMessage(messages.usernameLabel) }} {{ formatMessage(messages.usernameLabel) }}
@@ -126,6 +178,8 @@ @@ -133,6 +185,8 @@
<script setup lang="ts"> <script setup lang="ts">
import { import {
CheckIcon, CheckIcon,
@@ -111,7 +111,7 @@ index 4aab3f9..b67b2c7 100644
GlobeIcon, GlobeIcon,
KeyIcon, KeyIcon,
LogInIcon, LogInIcon,
@@ -138,13 +192,20 @@ import { @@ -145,13 +199,20 @@ import {
Chips, Chips,
commonMessages, commonMessages,
defineMessages, defineMessages,
@@ -133,7 +133,7 @@ index 4aab3f9..b67b2c7 100644
type Server = { type Server = {
api_root: string api_root: string
@@ -152,8 +213,16 @@ type Server = { @@ -159,8 +220,16 @@ type Server = {
implementation?: string implementation?: string
homepage?: string homepage?: string
register?: string register?: string
@@ -150,10 +150,10 @@ index 4aab3f9..b67b2c7 100644
const { formatMessage } = useVIntl() const { formatMessage } = useVIntl()
@@ -171,7 +240,15 @@ const profiles = ref<Profile[]>([]) @@ -179,7 +248,15 @@ const profile = ref<Profile | null>(null)
const profile = ref<Profile | null>(null)
const error = ref('') const error = ref('')
const submitting = ref(false) const submitting = ref(false)
const lookingUp = ref(false)
+const browser = ref<DeviceCode | null>(null) +const browser = ref<DeviceCode | null>(null)
+const browserStarting = ref(false) +const browserStarting = ref(false)
+const browserError = ref('') +const browserError = ref('')
@@ -165,9 +165,9 @@ index 4aab3f9..b67b2c7 100644
+let pollTimeout: ReturnType<typeof setTimeout> | undefined +let pollTimeout: ReturnType<typeof setTimeout> | undefined
function message(e: unknown) { function message(e: unknown) {
return typeof e === 'string' ? e : ((e as Error)?.message ?? formatMessage(messages.genericError)) const text =
@@ -187,6 +264,8 @@ watch(address, () => { @@ -199,6 +276,8 @@ watch(address, () => {
lookup++ lookingUp.value = false
server.value = null server.value = null
serverError.value = '' serverError.value = ''
+ stopBrowser() + stopBrowser()
@@ -175,7 +175,7 @@ index 4aab3f9..b67b2c7 100644
}) })
function show(event?: MouseEvent) { function show(event?: MouseEvent) {
@@ -199,6 +278,8 @@ function show(event?: MouseEvent) { @@ -211,6 +290,8 @@ function show(event?: MouseEvent) {
profile.value = null profile.value = null
error.value = '' error.value = ''
submitting.value = false submitting.value = false
@@ -184,7 +184,7 @@ index 4aab3f9..b67b2c7 100644
modal.value?.show(event) modal.value?.show(event)
void nextTick(() => { void nextTick(() => {
document.getElementById('authlib-account-server')?.focus() document.getElementById('authlib-account-server')?.focus()
@@ -219,6 +300,72 @@ async function lookUpServer() { @@ -235,6 +316,72 @@ async function lookUpServer() {
} }
} }
@@ -257,7 +257,7 @@ index 4aab3f9..b67b2c7 100644
async function submit() { async function submit() {
if (submitting.value) return if (submitting.value) return
@@ -282,6 +429,34 @@ const messages = defineMessages({ @@ -309,6 +456,34 @@ const messages = defineMessages({
id: 'app.authlib-account.password-label', id: 'app.authlib-account.password-label',
defaultMessage: 'Password', defaultMessage: 'Password',
}, },
@@ -935,10 +935,10 @@ index b916a6d..e9fa853 100644
return Ok(()); return Ok(());
} }
diff --git a/packages/app-lib/src/util/authlib_injector.rs b/packages/app-lib/src/util/authlib_injector.rs diff --git a/packages/app-lib/src/util/authlib_injector.rs b/packages/app-lib/src/util/authlib_injector.rs
index 709e6d0..8788563 100644 index 13591ef..487b0ae 100644
--- a/packages/app-lib/src/util/authlib_injector.rs --- a/packages/app-lib/src/util/authlib_injector.rs
+++ b/packages/app-lib/src/util/authlib_injector.rs +++ b/packages/app-lib/src/util/authlib_injector.rs
@@ -17,6 +17,90 @@ use crate::state::DirectoryInfo; @@ -17,6 +17,96 @@ use crate::state::DirectoryInfo;
use crate::util::fetch::{INSECURE_REQWEST_CLIENT, REQWEST_CLIENT}; use crate::util::fetch::{INSECURE_REQWEST_CLIENT, REQWEST_CLIENT};
use crate::util::io; use crate::util::io;
@@ -951,8 +951,11 @@ index 709e6d0..8788563 100644
+const CONNECT_CLIENT_IDS: &[(&str, &str)] = &[]; +const CONNECT_CLIENT_IDS: &[(&str, &str)] = &[];
+ +
+/// The scopes a Yggdrasil Connect sign-in for playing needs. +/// The scopes a Yggdrasil Connect sign-in for playing needs.
+const CONNECT_SCOPES: [&str; 3] = +const CONNECT_SCOPES: [&str; 3] = [
+ ["openid", "Yggdrasil.PlayerProfiles.Select", "Yggdrasil.Server.Join"]; + "openid",
+ "Yggdrasil.PlayerProfiles.Select",
+ "Yggdrasil.Server.Join",
+];
+ +
+/// How to sign in to a server with Yggdrasil Connect, OAuth's device flow on +/// How to sign in to a server with Yggdrasil Connect, OAuth's device flow on
+/// the server's own page. +/// the server's own page.
@@ -998,7 +1001,10 @@ index 709e6d0..8788563 100644
+ .iter() + .iter()
+ .all(|endpoint| endpoint.starts_with("https://")); + .all(|endpoint| endpoint.starts_with("https://"));
+ let supported = |scope: &str| { + let supported = |scope: &str| {
+ config.scopes_supported.iter().any(|supported| supported == scope) + config
+ .scopes_supported
+ .iter()
+ .any(|supported| supported == scope)
+ }; + };
+ if !https || !CONNECT_SCOPES.iter().all(|scope| supported(scope)) { + if !https || !CONNECT_SCOPES.iter().all(|scope| supported(scope)) {
+ return None; + return None;
@@ -1029,7 +1035,7 @@ index 709e6d0..8788563 100644
/// An authlib-injector server, as its own metadata describes it. /// An authlib-injector server, as its own metadata describes it.
#[derive(Serialize, Debug, Clone)] #[derive(Serialize, Debug, Clone)]
pub struct AuthlibServer { pub struct AuthlibServer {
@@ -27,6 +111,8 @@ pub struct AuthlibServer { @@ -27,6 +117,8 @@ pub struct AuthlibServer {
pub implementation: Option<String>, pub implementation: Option<String>,
pub homepage: Option<String>, pub homepage: Option<String>,
pub register: Option<String>, pub register: Option<String>,
@@ -1038,7 +1044,7 @@ index 709e6d0..8788563 100644
} }
#[derive(Deserialize)] #[derive(Deserialize)]
@@ -46,6 +132,8 @@ struct Meta { @@ -46,6 +138,8 @@ struct Meta {
implementation_name: Option<String>, implementation_name: Option<String>,
#[serde(default)] #[serde(default)]
links: Links, links: Links,
@@ -1047,7 +1053,7 @@ index 709e6d0..8788563 100644
} }
#[derive(Deserialize, Default)] #[derive(Deserialize, Default)]
@@ -115,6 +203,11 @@ pub async fn resolve_server(address: &str) -> crate::Result<AuthlibServer> { @@ -119,6 +213,11 @@ pub async fn resolve_server(address: &str) -> crate::Result<AuthlibServer> {
)) ))
})?; })?;
@@ -1059,7 +1065,7 @@ index 709e6d0..8788563 100644
Ok(AuthlibServer { Ok(AuthlibServer {
api_root: api_root.as_str().trim_end_matches('/').to_owned(), api_root: api_root.as_str().trim_end_matches('/').to_owned(),
name: metadata name: metadata
@@ -125,6 +218,7 @@ pub async fn resolve_server(address: &str) -> crate::Result<AuthlibServer> { @@ -129,6 +228,7 @@ pub async fn resolve_server(address: &str) -> crate::Result<AuthlibServer> {
implementation: metadata.meta.implementation_name, implementation: metadata.meta.implementation_name,
homepage: metadata.meta.links.homepage, homepage: metadata.meta.links.homepage,
register: metadata.meta.links.register, register: metadata.meta.links.register,