feat: add ely.by accounts
This commit is contained in:
1 parent
d7fdb05329
commit
d0528cc1b8
5 files changed
+820
-8
No files matched your search
@@ -1,7 +1,7 @@
|
|||||||
# Modrinth Enhanced
|
# Modrinth Enhanced
|
||||||
|
|
||||||
The [Modrinth App](https://github.com/modrinth/code), without advertising, without telemetry, and
|
The [Modrinth App](https://github.com/modrinth/code), without advertising, without telemetry, and
|
||||||
with offline accounts.
|
with offline and Ely.by accounts.
|
||||||
|
|
||||||
Everything else is deliberately left alone. This repository holds no forked source code — only a
|
Everything else is deliberately left alone. This repository holds no forked source code — only a
|
||||||
series of patches that are applied to an upstream release tag, built, and published. Whenever
|
series of patches that are applied to an upstream release tag, built, and published. Whenever
|
||||||
@@ -13,11 +13,13 @@ works.
|
|||||||
|
|
||||||
| Patch | What it does |
|
| Patch | What it does |
|
||||||
| ----------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------- |
|
| ----------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||||
| `0001-Rename-the-app-to-Modrinth-Enhanced` | Product name, binary name, window title, version label, and an "Enhanced" pill next to the wordmark. |
|
| `0001-Rename-the-app-to-Modrinth-Enhanced` | Product name, binary name, window title and version label. |
|
||||||
| `0002-Use-the-Modrinth-Enhanced-icon` | The Modrinth mark with a sparkle badge, rendered into every icon the bundles need. The vector source ships alongside them. |
|
| `0002-Use-the-Modrinth-Enhanced-icon` | The Modrinth mark with a sparkle badge on a green disc, rendered into every icon the bundles need. Both vector sources ship alongside them. |
|
||||||
| `0003-Remove-advertising` | The sidebar ad slot, the "Upgrade to Modrinth+" nag and the ad cookie consent prompt. The ad webview is never created. |
|
| `0003-Remove-advertising` | The sidebar ad slot, the "Upgrade to Modrinth+" nag and the ad cookie consent prompt. The ad webview is never created. |
|
||||||
| `0004-Remove-telemetry` | PostHog analytics, Sentry crash reporting, the Tally survey embed, and the playtime and server-play reports the launcher sends to Modrinth. |
|
| `0004-Remove-telemetry` | PostHog analytics, Sentry crash reporting, the Tally survey embeds, and the playtime and server-play reports the launcher sends to Modrinth. |
|
||||||
| `0005-Add-offline-accounts` | A second way to add a Minecraft account that never contacts Microsoft or Mojang. |
|
| `0005-Add-offline-accounts` | A way to add a Minecraft account that never contacts Microsoft or Mojang. |
|
||||||
|
| `0006-Hide-Modrinth-Servers-...` | Modrinth Servers off in the sidebar by default, and a news section that folds away and stays folded. |
|
||||||
|
| `0007-Add-Ely.by-accounts` | Sign in with Ely.by, launched through authlib-injector. |
|
||||||
|
|
||||||
### Offline accounts
|
### Offline accounts
|
||||||
|
|
||||||
@@ -31,6 +33,29 @@ in online mode reject them, as they do in every other launcher.
|
|||||||
|
|
||||||
Microsoft sign-in is untouched and still the default.
|
Microsoft sign-in is untouched and still the default.
|
||||||
|
|
||||||
|
### Ely.by accounts
|
||||||
|
|
||||||
|
"Add Ely.by account" sits in the same account card. It asks for an Ely.by account name or email and
|
||||||
|
a password, which go to `authserver.ely.by` and nowhere else. With two-factor authentication on,
|
||||||
|
append the current code to the password after a colon, which is Ely.by's own convention.
|
||||||
|
|
||||||
|
At launch the game is pointed at Ely.by with
|
||||||
|
[authlib-injector](https://github.com/yushijinhun/authlib-injector), downloaded once and cached, so
|
||||||
|
such an account can play singleplayer and join any server that accepts Ely.by.
|
||||||
|
|
||||||
|
The account is stored in the same table as every other one, marked by the client token Ely.by
|
||||||
|
issues; the token pair is checked and renewed against Ely.by a few times a day rather than on every
|
||||||
|
read of the account list.
|
||||||
|
|
||||||
|
Signing in on Ely.by's own page instead of in this form would be better, and needs an OAuth
|
||||||
|
application registered with Ely.by — one has not been registered for Modrinth Enhanced.
|
||||||
|
|
||||||
|
### Sidebar and news
|
||||||
|
|
||||||
|
Modrinth Servers is hidden from the left sidebar by default and can be switched back on under
|
||||||
|
Settings > Features > Sidebar. The news section in the right sidebar folds away by clicking its
|
||||||
|
heading and stays that way across restarts.
|
||||||
|
|
||||||
### What is *not* removed
|
### What is *not* removed
|
||||||
|
|
||||||
Download attribution still happens. It is a header on downloads you already asked for, and it is
|
Download attribution still happens. It is a header on downloads you already asked for, and it is
|
||||||
|
|||||||
@@ -22,6 +22,7 @@ data when they are opened elsewhere.
|
|||||||
Usernames are validated the way Mojang validates them: 3 to 16
|
Usernames are validated the way Mojang validates them: 3 to 16
|
||||||
characters of letters, numbers and underscores.
|
characters of letters, numbers and underscores.
|
||||||
---
|
---
|
||||||
|
Cargo.lock | 1 +
|
||||||
Cargo.toml | 1 +
|
Cargo.toml | 1 +
|
||||||
.../src/components/ui/AccountsCard.vue | 25 ++++
|
.../src/components/ui/AccountsCard.vue | 25 ++++
|
||||||
.../src/components/ui/OfflineAccountModal.vue | 136 ++++++++++++++++++
|
.../src/components/ui/OfflineAccountModal.vue | 136 ++++++++++++++++++
|
||||||
@@ -30,9 +31,21 @@ characters of letters, numbers and underscores.
|
|||||||
packages/app-lib/Cargo.toml | 1 +
|
packages/app-lib/Cargo.toml | 1 +
|
||||||
packages/app-lib/src/api/minecraft_auth.rs | 39 +++++
|
packages/app-lib/src/api/minecraft_auth.rs | 39 +++++
|
||||||
packages/app-lib/src/state/minecraft_auth.rs | 68 +++++++++
|
packages/app-lib/src/state/minecraft_auth.rs | 68 +++++++++
|
||||||
8 files changed, 290 insertions(+)
|
9 files changed, 291 insertions(+)
|
||||||
create mode 100644 apps/app-frontend/src/components/ui/OfflineAccountModal.vue
|
create mode 100644 apps/app-frontend/src/components/ui/OfflineAccountModal.vue
|
||||||
|
|
||||||
|
diff --git a/Cargo.lock b/Cargo.lock
|
||||||
|
index d40a959..95bc842 100644
|
||||||
|
--- a/Cargo.lock
|
||||||
|
+++ b/Cargo.lock
|
||||||
|
@@ -11384,6 +11384,7 @@ dependencies = [
|
||||||
|
"indicatif",
|
||||||
|
"itertools 0.14.0",
|
||||||
|
"json5",
|
||||||
|
+ "md-5",
|
||||||
|
"modrinth-content-management",
|
||||||
|
"notify",
|
||||||
|
"notify-debouncer-mini",
|
||||||
diff --git a/Cargo.toml b/Cargo.toml
|
diff --git a/Cargo.toml b/Cargo.toml
|
||||||
index a4a779c..a85f576 100644
|
index a4a779c..a85f576 100644
|
||||||
--- a/Cargo.toml
|
--- a/Cargo.toml
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ restart without needing a settings migration:
|
|||||||
4 files changed, 59 insertions(+), 3 deletions(-)
|
4 files changed, 59 insertions(+), 3 deletions(-)
|
||||||
|
|
||||||
diff --git a/apps/app-frontend/src/App.vue b/apps/app-frontend/src/App.vue
|
diff --git a/apps/app-frontend/src/App.vue b/apps/app-frontend/src/App.vue
|
||||||
index 33c29ce..4c3663f 100644
|
index 33c29ce..1cab873 100644
|
||||||
--- a/apps/app-frontend/src/App.vue
|
--- a/apps/app-frontend/src/App.vue
|
||||||
+++ b/apps/app-frontend/src/App.vue
|
+++ b/apps/app-frontend/src/App.vue
|
||||||
@@ -11,6 +11,7 @@ import {
|
@@ -11,6 +11,7 @@ import {
|
||||||
@@ -39,7 +39,7 @@ index 33c29ce..4c3663f 100644
|
|||||||
+// remembers its collapsed sections.
|
+// remembers its collapsed sections.
|
||||||
+const newsCollapsed = computed(() => appSettings.getFeatureFlag('news_collapsed'))
|
+const newsCollapsed = computed(() => appSettings.getFeatureFlag('news_collapsed'))
|
||||||
+
|
+
|
||||||
+function setNewsCollapsed(collapsed: boolean) {
|
+function setNewsCollapsed(collapsed) {
|
||||||
+ appSettings.featureFlags.news_collapsed = collapsed
|
+ appSettings.featureFlags.news_collapsed = collapsed
|
||||||
+ getSettings()
|
+ getSettings()
|
||||||
+ .then((settings) => {
|
+ .then((settings) => {
|
||||||
|
|||||||
@@ -0,0 +1,760 @@
|
|||||||
|
From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001
|
||||||
|
From: Modrinth Enhanced <patches@modrinth-enhanced.invalid>
|
||||||
|
Date: Mon, 14 Sep 2026 13:24:41 +0200
|
||||||
|
Subject: [PATCH] Add Ely.by accounts
|
||||||
|
|
||||||
|
Ely.by is an alternative Minecraft account system. "Add Ely.by account"
|
||||||
|
sits next to the Microsoft and offline options in the account card; the
|
||||||
|
account name and password go to authserver.ely.by and nowhere else, and
|
||||||
|
a two-factor code is appended to the password after a colon, which is
|
||||||
|
Ely.by's own convention.
|
||||||
|
|
||||||
|
Minecraft asks Mojang who the player is, so an Ely.by account cannot
|
||||||
|
start the game on its own. The authlib-injector agent points those calls
|
||||||
|
at Ely.by instead; it is downloaded once and cached, after which such an
|
||||||
|
account launches with no network at all.
|
||||||
|
|
||||||
|
Like offline accounts, an Ely.by account is a row in `minecraft_users`
|
||||||
|
rather than a table of its own, so no migration is needed: the client
|
||||||
|
token Ely.by issues alongside the access token lives behind a marker in
|
||||||
|
the refresh token column, which is both what renews the pair and what
|
||||||
|
identifies the account. The expiry column becomes "check again after" -
|
||||||
|
Ely.by does not say when its tokens expire - so the pair is validated
|
||||||
|
and, if needed, renewed a few times a day instead of on every read of
|
||||||
|
the account list.
|
||||||
|
|
||||||
|
Signing in on Ely.by's own page would be better than a password form,
|
||||||
|
and needs an OAuth application registered with Ely.by; there is none for
|
||||||
|
Modrinth Enhanced yet.
|
||||||
|
---
|
||||||
|
.../src/components/ui/AccountsCard.vue | 25 +-
|
||||||
|
.../src/components/ui/ElyAccountModal.vue | 166 ++++++++++++++
|
||||||
|
apps/app-frontend/src/helpers/auth.js | 15 ++
|
||||||
|
apps/app/src/api/auth.rs | 10 +
|
||||||
|
packages/app-lib/src/api/minecraft_auth.rs | 34 +++
|
||||||
|
packages/app-lib/src/launcher/mod.rs | 16 ++
|
||||||
|
packages/app-lib/src/state/minecraft_auth.rs | 213 +++++++++++++++++-
|
||||||
|
packages/app-lib/src/util/authlib_injector.rs | 77 +++++++
|
||||||
|
packages/app-lib/src/util/mod.rs | 1 +
|
||||||
|
9 files changed, 551 insertions(+), 6 deletions(-)
|
||||||
|
create mode 100644 apps/app-frontend/src/components/ui/ElyAccountModal.vue
|
||||||
|
create mode 100644 packages/app-lib/src/util/authlib_injector.rs
|
||||||
|
|
||||||
|
diff --git a/apps/app-frontend/src/components/ui/AccountsCard.vue b/apps/app-frontend/src/components/ui/AccountsCard.vue
|
||||||
|
index e695a6d..3f694db 100644
|
||||||
|
--- a/apps/app-frontend/src/components/ui/AccountsCard.vue
|
||||||
|
+++ b/apps/app-frontend/src/components/ui/AccountsCard.vue
|
||||||
|
@@ -9,6 +9,10 @@
|
||||||
|
<SpinnerIcon v-else class="animate-spin" />
|
||||||
|
{{ formatMessage(messages.signInToMinecraft) }}
|
||||||
|
</Button>
|
||||||
|
+ <Button @click="elyAccountModal?.show($event)">
|
||||||
|
+ <KeyIcon />
|
||||||
|
+ {{ formatMessage(messages.addElyAccount) }}
|
||||||
|
+ </Button>
|
||||||
|
<Button @click="offlineAccountModal?.show($event)">
|
||||||
|
<UserIcon />
|
||||||
|
{{ formatMessage(messages.addOfflineAccount) }}
|
||||||
|
@@ -84,6 +88,13 @@
|
||||||
|
<PlusIcon />
|
||||||
|
{{ formatMessage(messages.addAccount) }}
|
||||||
|
</Button>
|
||||||
|
+ <Button
|
||||||
|
+ class="w-full !bg-button-bg !text-primary ![box-shadow:var(--shadow-button)]"
|
||||||
|
+ @click="elyAccountModal?.show($event)"
|
||||||
|
+ >
|
||||||
|
+ <KeyIcon />
|
||||||
|
+ {{ formatMessage(messages.addElyAccount) }}
|
||||||
|
+ </Button>
|
||||||
|
<Button
|
||||||
|
class="w-full !bg-button-bg !text-primary ![box-shadow:var(--shadow-button)]"
|
||||||
|
@click="offlineAccountModal?.show($event)"
|
||||||
|
@@ -94,11 +105,13 @@
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Accordion>
|
||||||
|
- <OfflineAccountModal ref="offlineAccountModal" @created="offlineAccountCreated" />
|
||||||
|
+ <OfflineAccountModal ref="offlineAccountModal" @created="accountAdded" />
|
||||||
|
+ <ElyAccountModal ref="elyAccountModal" @created="accountAdded" />
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import {
|
||||||
|
+ KeyIcon,
|
||||||
|
LogInIcon,
|
||||||
|
PlusIcon,
|
||||||
|
RadioButtonCheckedIcon,
|
||||||
|
@@ -119,6 +132,7 @@ import {
|
||||||
|
import type { Ref } from 'vue'
|
||||||
|
import { computed, onUnmounted, ref } from 'vue'
|
||||||
|
|
||||||
|
+import ElyAccountModal from '@/components/ui/ElyAccountModal.vue'
|
||||||
|
import OfflineAccountModal from '@/components/ui/OfflineAccountModal.vue'
|
||||||
|
import { useAppEvent } from '@/composables/use-app-event'
|
||||||
|
import { handleSevereError } from '@/composables/use-error.js'
|
||||||
|
@@ -150,6 +164,7 @@ type MinecraftCredential = {
|
||||||
|
|
||||||
|
const accounts: Ref<MinecraftCredential[]> = ref([])
|
||||||
|
const offlineAccountModal = ref<InstanceType<typeof OfflineAccountModal>>()
|
||||||
|
+const elyAccountModal = ref<InstanceType<typeof ElyAccountModal>>()
|
||||||
|
const loginDisabled = ref(false)
|
||||||
|
const defaultUser = ref<string | undefined>()
|
||||||
|
const equippedSkin = ref<Skin | null>(null)
|
||||||
|
@@ -262,8 +277,8 @@ async function login() {
|
||||||
|
loginDisabled.value = false
|
||||||
|
}
|
||||||
|
|
||||||
|
-async function offlineAccountCreated() {
|
||||||
|
- // `login_offline` already marks the new account as the active one.
|
||||||
|
+async function accountAdded() {
|
||||||
|
+ // Both sign-in paths already mark the new account as the active one.
|
||||||
|
await refreshValues()
|
||||||
|
emit('change')
|
||||||
|
}
|
||||||
|
@@ -298,6 +313,10 @@ const messages = defineMessages({
|
||||||
|
id: 'minecraft-account.add-offline-account',
|
||||||
|
defaultMessage: 'Add offline account',
|
||||||
|
},
|
||||||
|
+ addElyAccount: {
|
||||||
|
+ id: 'minecraft-account.add-ely-account',
|
||||||
|
+ defaultMessage: 'Add Ely.by account',
|
||||||
|
+ },
|
||||||
|
removeAccount: {
|
||||||
|
id: 'minecraft-account.remove-account',
|
||||||
|
defaultMessage: 'Remove account',
|
||||||
|
diff --git a/apps/app-frontend/src/components/ui/ElyAccountModal.vue b/apps/app-frontend/src/components/ui/ElyAccountModal.vue
|
||||||
|
new file mode 100644
|
||||||
|
index 0000000..d909ec3
|
||||||
|
--- /dev/null
|
||||||
|
+++ b/apps/app-frontend/src/components/ui/ElyAccountModal.vue
|
||||||
|
@@ -0,0 +1,166 @@
|
||||||
|
+<template>
|
||||||
|
+ <NewModal ref="modal" :header="formatMessage(messages.header)" max-width="480px" width="100%">
|
||||||
|
+ <div class="flex flex-col gap-4">
|
||||||
|
+ <p class="m-0 leading-tight text-secondary">
|
||||||
|
+ {{ formatMessage(messages.description) }}
|
||||||
|
+ </p>
|
||||||
|
+
|
||||||
|
+ <form class="flex flex-col gap-3" @submit.prevent="submit">
|
||||||
|
+ <div class="flex flex-col gap-2">
|
||||||
|
+ <label class="font-semibold text-contrast" for="ely-account-username">
|
||||||
|
+ {{ formatMessage(messages.usernameLabel) }}
|
||||||
|
+ </label>
|
||||||
|
+ <Input
|
||||||
|
+ id="ely-account-username"
|
||||||
|
+ v-model="username"
|
||||||
|
+ :icon="UserIcon"
|
||||||
|
+ :placeholder="formatMessage(messages.usernamePlaceholder)"
|
||||||
|
+ :error="!!error"
|
||||||
|
+ autocapitalize="none"
|
||||||
|
+ autocorrect="off"
|
||||||
|
+ :spellcheck="false"
|
||||||
|
+ class="w-full"
|
||||||
|
+ />
|
||||||
|
+ </div>
|
||||||
|
+
|
||||||
|
+ <div class="flex flex-col gap-2">
|
||||||
|
+ <label class="font-semibold text-contrast" for="ely-account-password">
|
||||||
|
+ {{ formatMessage(messages.passwordLabel) }}
|
||||||
|
+ </label>
|
||||||
|
+ <Input
|
||||||
|
+ id="ely-account-password"
|
||||||
|
+ v-model="password"
|
||||||
|
+ type="password"
|
||||||
|
+ :icon="KeyIcon"
|
||||||
|
+ :error="!!error"
|
||||||
|
+ class="w-full"
|
||||||
|
+ />
|
||||||
|
+ <p class="m-0 text-sm leading-tight text-secondary">
|
||||||
|
+ {{ formatMessage(messages.twoFactorHint) }}
|
||||||
|
+ </p>
|
||||||
|
+ </div>
|
||||||
|
+
|
||||||
|
+ <p v-if="error" class="m-0 text-sm leading-tight text-red">{{ error }}</p>
|
||||||
|
+ </form>
|
||||||
|
+ </div>
|
||||||
|
+
|
||||||
|
+ <template #actions>
|
||||||
|
+ <div class="flex justify-end gap-2">
|
||||||
|
+ <Button native-type="button" @click="modal?.hide()">
|
||||||
|
+ <XIcon aria-hidden="true" />
|
||||||
|
+ {{ formatMessage(commonMessages.cancelButton) }}
|
||||||
|
+ </Button>
|
||||||
|
+ <Button
|
||||||
|
+ type="colored"
|
||||||
|
+ color="brand"
|
||||||
|
+ native-type="button"
|
||||||
|
+ :disabled="submitting || !username.trim() || !password"
|
||||||
|
+ @click="submit"
|
||||||
|
+ >
|
||||||
|
+ <SpinnerIcon v-if="submitting" aria-hidden="true" class="animate-spin" />
|
||||||
|
+ <LogInIcon v-else aria-hidden="true" />
|
||||||
|
+ {{ formatMessage(messages.signInButton) }}
|
||||||
|
+ </Button>
|
||||||
|
+ </div>
|
||||||
|
+ </template>
|
||||||
|
+ </NewModal>
|
||||||
|
+</template>
|
||||||
|
+
|
||||||
|
+<script setup lang="ts">
|
||||||
|
+import { KeyIcon, LogInIcon, SpinnerIcon, UserIcon, XIcon } from '@modrinth/assets'
|
||||||
|
+import {
|
||||||
|
+ Button,
|
||||||
|
+ commonMessages,
|
||||||
|
+ defineMessages,
|
||||||
|
+ Input,
|
||||||
|
+ NewModal,
|
||||||
|
+ useVIntl,
|
||||||
|
+} from '@modrinth/ui'
|
||||||
|
+import { nextTick, ref } from 'vue'
|
||||||
|
+
|
||||||
|
+import { login_ely } from '@/helpers/auth'
|
||||||
|
+
|
||||||
|
+const { formatMessage } = useVIntl()
|
||||||
|
+
|
||||||
|
+const emit = defineEmits<{
|
||||||
|
+ created: [account: unknown]
|
||||||
|
+}>()
|
||||||
|
+
|
||||||
|
+const modal = ref<InstanceType<typeof NewModal>>()
|
||||||
|
+const username = ref('')
|
||||||
|
+const password = ref('')
|
||||||
|
+const error = ref('')
|
||||||
|
+const submitting = ref(false)
|
||||||
|
+
|
||||||
|
+function show(event?: MouseEvent) {
|
||||||
|
+ username.value = ''
|
||||||
|
+ password.value = ''
|
||||||
|
+ error.value = ''
|
||||||
|
+ submitting.value = false
|
||||||
|
+ modal.value?.show(event)
|
||||||
|
+ void nextTick(() => {
|
||||||
|
+ document.getElementById('ely-account-username')?.focus()
|
||||||
|
+ })
|
||||||
|
+}
|
||||||
|
+
|
||||||
|
+async function submit() {
|
||||||
|
+ if (submitting.value) return
|
||||||
|
+
|
||||||
|
+ const name = username.value.trim()
|
||||||
|
+ if (!name || !password.value) return
|
||||||
|
+
|
||||||
|
+ submitting.value = true
|
||||||
|
+ error.value = ''
|
||||||
|
+
|
||||||
|
+ try {
|
||||||
|
+ const account = await login_ely(name, password.value)
|
||||||
|
+ password.value = ''
|
||||||
|
+ modal.value?.hide()
|
||||||
|
+ emit('created', account)
|
||||||
|
+ } catch (e) {
|
||||||
|
+ error.value =
|
||||||
|
+ typeof e === 'string' ? e : ((e as Error)?.message ?? formatMessage(messages.genericError))
|
||||||
|
+ } finally {
|
||||||
|
+ submitting.value = false
|
||||||
|
+ }
|
||||||
|
+}
|
||||||
|
+
|
||||||
|
+defineExpose({ show })
|
||||||
|
+
|
||||||
|
+const messages = defineMessages({
|
||||||
|
+ header: {
|
||||||
|
+ id: 'app.ely-account.header',
|
||||||
|
+ defaultMessage: 'Sign in with Ely.by',
|
||||||
|
+ },
|
||||||
|
+ description: {
|
||||||
|
+ id: 'app.ely-account.description',
|
||||||
|
+ defaultMessage:
|
||||||
|
+ 'Ely.by is an alternative Minecraft account system. Your credentials go to Ely.by and nowhere else; the game is pointed at it with authlib-injector when the instance launches.',
|
||||||
|
+ },
|
||||||
|
+ usernameLabel: {
|
||||||
|
+ id: 'app.ely-account.username-label',
|
||||||
|
+ defaultMessage: 'Account name or email',
|
||||||
|
+ },
|
||||||
|
+ usernamePlaceholder: {
|
||||||
|
+ id: 'app.ely-account.username-placeholder',
|
||||||
|
+ defaultMessage: 'Your Ely.by account',
|
||||||
|
+ },
|
||||||
|
+ passwordLabel: {
|
||||||
|
+ id: 'app.ely-account.password-label',
|
||||||
|
+ defaultMessage: 'Password',
|
||||||
|
+ },
|
||||||
|
+ twoFactorHint: {
|
||||||
|
+ id: 'app.ely-account.two-factor-hint',
|
||||||
|
+ defaultMessage:
|
||||||
|
+ 'With two-factor authentication on, append your current code to the password, separated by a colon.',
|
||||||
|
+ },
|
||||||
|
+ signInButton: {
|
||||||
|
+ id: 'app.ely-account.sign-in-button',
|
||||||
|
+ defaultMessage: 'Sign in',
|
||||||
|
+ },
|
||||||
|
+ genericError: {
|
||||||
|
+ id: 'app.ely-account.generic-error',
|
||||||
|
+ defaultMessage: 'Could not sign in to Ely.by.',
|
||||||
|
+ },
|
||||||
|
+})
|
||||||
|
+</script>
|
||||||
|
diff --git a/apps/app-frontend/src/helpers/auth.js b/apps/app-frontend/src/helpers/auth.js
|
||||||
|
index cb7319a..57580ff 100644
|
||||||
|
--- a/apps/app-frontend/src/helpers/auth.js
|
||||||
|
+++ b/apps/app-frontend/src/helpers/auth.js
|
||||||
|
@@ -46,6 +46,21 @@ export async function login_offline(username) {
|
||||||
|
return await invoke('plugin:auth|login_offline', { username })
|
||||||
|
}
|
||||||
|
|
||||||
|
+/**
|
||||||
|
+ * Signs in to Ely.by and makes that account the active one.
|
||||||
|
+ *
|
||||||
|
+ * Ely.by is an alternative Minecraft account system. The game is pointed at it
|
||||||
|
+ * with the authlib-injector agent at launch.
|
||||||
|
+ *
|
||||||
|
+ * @param {string} username Ely.by account name or email
|
||||||
|
+ * @param {string} password Ely.by password, with `:code` appended when the
|
||||||
|
+ * account has two-factor authentication enabled
|
||||||
|
+ * @returns {Promise<Credential>}
|
||||||
|
+ */
|
||||||
|
+export async function login_ely(username, password) {
|
||||||
|
+ return await invoke('plugin:auth|login_ely', { username, password })
|
||||||
|
+}
|
||||||
|
+
|
||||||
|
/**
|
||||||
|
* Retrieves the default user
|
||||||
|
* @return {Promise<UUID | undefined>}
|
||||||
|
diff --git a/apps/app/src/api/auth.rs b/apps/app/src/api/auth.rs
|
||||||
|
index f4eded6..dea07b2 100644
|
||||||
|
--- a/apps/app/src/api/auth.rs
|
||||||
|
+++ b/apps/app/src/api/auth.rs
|
||||||
|
@@ -10,6 +10,7 @@ pub fn init<R: Runtime>() -> TauriPlugin<R> {
|
||||||
|
check_reachable,
|
||||||
|
login,
|
||||||
|
login_offline,
|
||||||
|
+ login_ely,
|
||||||
|
remove_user,
|
||||||
|
get_default_user,
|
||||||
|
set_default_user,
|
||||||
|
@@ -93,6 +94,15 @@ pub async fn login_offline(username: String) -> Result<Credentials> {
|
||||||
|
Ok(minecraft_auth::login_offline(&username).await?)
|
||||||
|
}
|
||||||
|
|
||||||
|
+/// Signs in to Ely.by and makes that account active.
|
||||||
|
+#[tauri::command]
|
||||||
|
+pub async fn login_ely(
|
||||||
|
+ username: String,
|
||||||
|
+ password: String,
|
||||||
|
+) -> Result<Credentials> {
|
||||||
|
+ Ok(minecraft_auth::login_ely(&username, &password).await?)
|
||||||
|
+}
|
||||||
|
+
|
||||||
|
#[tauri::command]
|
||||||
|
pub async fn remove_user(user: uuid::Uuid) -> Result<()> {
|
||||||
|
Ok(minecraft_auth::remove_user(user).await?)
|
||||||
|
diff --git a/packages/app-lib/src/api/minecraft_auth.rs b/packages/app-lib/src/api/minecraft_auth.rs
|
||||||
|
index a7fac4a..2d18da3 100644
|
||||||
|
--- a/packages/app-lib/src/api/minecraft_auth.rs
|
||||||
|
+++ b/packages/app-lib/src/api/minecraft_auth.rs
|
||||||
|
@@ -86,6 +86,40 @@ pub async fn login_offline(username: &str) -> crate::Result<Credentials> {
|
||||||
|
Ok(credentials)
|
||||||
|
}
|
||||||
|
|
||||||
|
+/// Signs in to Ely.by and makes the account the active one.
|
||||||
|
+///
|
||||||
|
+/// Ely.by is an alternative account system for Minecraft. The game is pointed
|
||||||
|
+/// at it with the authlib-injector agent at launch, so such an account can play
|
||||||
|
+/// singleplayer and join any server that accepts Ely.by.
|
||||||
|
+#[tracing::instrument(skip(password))]
|
||||||
|
+pub async fn login_ely(
|
||||||
|
+ username: &str,
|
||||||
|
+ password: &str,
|
||||||
|
+) -> crate::Result<Credentials> {
|
||||||
|
+ let username = username.trim();
|
||||||
|
+
|
||||||
|
+ if username.is_empty() || password.is_empty() {
|
||||||
|
+ return Err(crate::ErrorKind::InputError(
|
||||||
|
+ "An Ely.by account name and password are both required".to_string(),
|
||||||
|
+ )
|
||||||
|
+ .into());
|
||||||
|
+ }
|
||||||
|
+
|
||||||
|
+ let state = State::get().await?;
|
||||||
|
+ let credentials = Credentials::ely(username, password).await?;
|
||||||
|
+ credentials.upsert(&state.pool).await?;
|
||||||
|
+
|
||||||
|
+ if let Err(error) =
|
||||||
|
+ crate::onboarding_checklist::mark_logged_into_minecraft().await
|
||||||
|
+ {
|
||||||
|
+ tracing::warn!(
|
||||||
|
+ "Failed to mark Minecraft login in onboarding checklist: {error}"
|
||||||
|
+ );
|
||||||
|
+ }
|
||||||
|
+
|
||||||
|
+ Ok(credentials)
|
||||||
|
+}
|
||||||
|
+
|
||||||
|
#[tracing::instrument]
|
||||||
|
pub async fn get_default_user() -> crate::Result<Option<uuid::Uuid>> {
|
||||||
|
let state = State::get().await?;
|
||||||
|
diff --git a/packages/app-lib/src/launcher/mod.rs b/packages/app-lib/src/launcher/mod.rs
|
||||||
|
index b482547..660f63b 100644
|
||||||
|
--- a/packages/app-lib/src/launcher/mod.rs
|
||||||
|
+++ b/packages/app-lib/src/launcher/mod.rs
|
||||||
|
@@ -1056,6 +1056,22 @@ pub async fn launch_minecraft(
|
||||||
|
command.arg("--add-opens=jdk.internal/jdk.internal.misc=ALL-UNNAMED");
|
||||||
|
}
|
||||||
|
|
||||||
|
+ // Minecraft asks Mojang who the player is, and an Ely.by account is not a
|
||||||
|
+ // Mojang account. authlib-injector is a Java agent that points those calls
|
||||||
|
+ // at Ely.by instead, and without it such an account cannot start the game.
|
||||||
|
+ if credentials.is_ely() {
|
||||||
|
+ let injector = crate::util::authlib_injector::get_authlib_injector(
|
||||||
|
+ &state.directories,
|
||||||
|
+ )
|
||||||
|
+ .await?;
|
||||||
|
+
|
||||||
|
+ command.arg(format!(
|
||||||
|
+ "-javaagent:{}={}",
|
||||||
|
+ injector.to_string_lossy(),
|
||||||
|
+ crate::state::ELY_API_ROOT
|
||||||
|
+ ));
|
||||||
|
+ }
|
||||||
|
+
|
||||||
|
command
|
||||||
|
.arg("com.modrinth.theseus.MinecraftLaunch")
|
||||||
|
.arg(version_info.main_class.clone())
|
||||||
|
diff --git a/packages/app-lib/src/state/minecraft_auth.rs b/packages/app-lib/src/state/minecraft_auth.rs
|
||||||
|
index d97d233..25c6dfb 100644
|
||||||
|
--- a/packages/app-lib/src/state/minecraft_auth.rs
|
||||||
|
+++ b/packages/app-lib/src/state/minecraft_auth.rs
|
||||||
|
@@ -241,6 +241,67 @@ pub fn offline_uuid(username: &str) -> Uuid {
|
||||||
|
Uuid::from_bytes(bytes)
|
||||||
|
}
|
||||||
|
|
||||||
|
+/// Marker stored in front of an Ely.by account's client token.
|
||||||
|
+///
|
||||||
|
+/// Ely.by's Yggdrasil flow hands back an access token and a client token, and
|
||||||
|
+/// renewing the pair needs both. The client token therefore goes in the refresh
|
||||||
|
+/// token column behind this marker, which both identifies the account as an
|
||||||
|
+/// Ely.by one and keeps it out of a table of its own.
|
||||||
|
+const ELY_REFRESH_TOKEN_PREFIX: &str = "modrinth-enhanced:ely:";
|
||||||
|
+
|
||||||
|
+/// Ely.by's Yggdrasil server, which answers the same shapes Mojang's used to.
|
||||||
|
+const ELY_AUTHSERVER: &str = "https://authserver.ely.by";
|
||||||
|
+
|
||||||
|
+/// What authlib-injector is handed so the game asks Ely.by rather than Mojang.
|
||||||
|
+///
|
||||||
|
+/// The agent resolves the short form to Ely.by's actual API root itself.
|
||||||
|
+pub const ELY_API_ROOT: &str = "ely.by";
|
||||||
|
+
|
||||||
|
+#[derive(Deserialize)]
|
||||||
|
+struct ElyAuthResponse {
|
||||||
|
+ #[serde(rename = "accessToken")]
|
||||||
|
+ access_token: String,
|
||||||
|
+ #[serde(rename = "clientToken")]
|
||||||
|
+ client_token: String,
|
||||||
|
+ #[serde(rename = "selectedProfile")]
|
||||||
|
+ selected_profile: ElyProfile,
|
||||||
|
+}
|
||||||
|
+
|
||||||
|
+#[derive(Deserialize)]
|
||||||
|
+struct ElyProfile {
|
||||||
|
+ id: String,
|
||||||
|
+ name: String,
|
||||||
|
+}
|
||||||
|
+
|
||||||
|
+/// Reads the error message out of an Ely.by refusal, falling back to the status.
|
||||||
|
+async fn ely_error(response: Response) -> crate::Error {
|
||||||
|
+ #[derive(Deserialize)]
|
||||||
|
+ struct ElyError {
|
||||||
|
+ #[serde(rename = "errorMessage")]
|
||||||
|
+ error_message: Option<String>,
|
||||||
|
+ }
|
||||||
|
+
|
||||||
|
+ let status = response.status();
|
||||||
|
+ let message = response
|
||||||
|
+ .json::<ElyError>()
|
||||||
|
+ .await
|
||||||
|
+ .ok()
|
||||||
|
+ .and_then(|error| error.error_message)
|
||||||
|
+ .unwrap_or_else(|| format!("Ely.by refused the request ({status})"));
|
||||||
|
+
|
||||||
|
+ crate::ErrorKind::OtherError(message).as_error()
|
||||||
|
+}
|
||||||
|
+
|
||||||
|
+/// Ely.by's Yggdrasil UUIDs come without dashes.
|
||||||
|
+fn parse_ely_uuid(id: &str) -> crate::Result<Uuid> {
|
||||||
|
+ Uuid::parse_str(id).map_err(|_| {
|
||||||
|
+ crate::ErrorKind::OtherError(format!(
|
||||||
|
+ "Ely.by returned a player id that could not be read: {id}"
|
||||||
|
+ ))
|
||||||
|
+ .as_error()
|
||||||
|
+ })
|
||||||
|
+}
|
||||||
|
+
|
||||||
|
/// An entry in the player profile cache, keyed by player UUID.
|
||||||
|
pub(super) enum ProfileCacheEntry {
|
||||||
|
/// A cached profile that is valid, even though it may be stale.
|
||||||
|
@@ -321,6 +382,135 @@ impl Credentials {
|
||||||
|
self.refresh_token == OFFLINE_REFRESH_TOKEN
|
||||||
|
}
|
||||||
|
|
||||||
|
+ /// Whether these credentials belong to an Ely.by account.
|
||||||
|
+ pub fn is_ely(&self) -> bool {
|
||||||
|
+ self.refresh_token.starts_with(ELY_REFRESH_TOKEN_PREFIX)
|
||||||
|
+ }
|
||||||
|
+
|
||||||
|
+ /// The client token Ely.by issued with the access token, if this is an
|
||||||
|
+ /// Ely.by account.
|
||||||
|
+ fn ely_client_token(&self) -> Option<&str> {
|
||||||
|
+ self.refresh_token.strip_prefix(ELY_REFRESH_TOKEN_PREFIX)
|
||||||
|
+ }
|
||||||
|
+
|
||||||
|
+ /// Signs in to Ely.by with an account name or email and a password.
|
||||||
|
+ ///
|
||||||
|
+ /// Ely.by accounts with two-factor authentication expect the current code
|
||||||
|
+ /// appended to the password with a colon, which is Ely.by's own convention
|
||||||
|
+ /// and is passed straight through.
|
||||||
|
+ pub async fn ely(username: &str, password: &str) -> crate::Result<Self> {
|
||||||
|
+ let client_token = Uuid::new_v4().to_string();
|
||||||
|
+
|
||||||
|
+ let response = INSECURE_REQWEST_CLIENT
|
||||||
|
+ .post(format!("{ELY_AUTHSERVER}/auth/authenticate"))
|
||||||
|
+ .json(&json!({
|
||||||
|
+ "username": username,
|
||||||
|
+ "password": password,
|
||||||
|
+ "clientToken": client_token,
|
||||||
|
+ "requestUser": false,
|
||||||
|
+ "agent": { "name": "Minecraft", "version": 1 },
|
||||||
|
+ }))
|
||||||
|
+ .send()
|
||||||
|
+ .await
|
||||||
|
+ .map_err(|error| {
|
||||||
|
+ crate::ErrorKind::OtherError(format!(
|
||||||
|
+ "Could not reach Ely.by: {error}"
|
||||||
|
+ ))
|
||||||
|
+ })?;
|
||||||
|
+
|
||||||
|
+ if !response.status().is_success() {
|
||||||
|
+ return Err(ely_error(response).await);
|
||||||
|
+ }
|
||||||
|
+
|
||||||
|
+ let auth = response.json::<ElyAuthResponse>().await?;
|
||||||
|
+
|
||||||
|
+ Ok(Self {
|
||||||
|
+ offline_profile: MinecraftProfile {
|
||||||
|
+ id: parse_ely_uuid(&auth.selected_profile.id)?,
|
||||||
|
+ name: auth.selected_profile.name,
|
||||||
|
+ ..MinecraftProfile::default()
|
||||||
|
+ },
|
||||||
|
+ access_token: auth.access_token,
|
||||||
|
+ refresh_token: format!(
|
||||||
|
+ "{ELY_REFRESH_TOKEN_PREFIX}{}",
|
||||||
|
+ auth.client_token
|
||||||
|
+ ),
|
||||||
|
+ // Ely.by does not say when its token expires. The expiry column is
|
||||||
|
+ // used as "check again after" instead, so the launcher asks Ely.by
|
||||||
|
+ // about the token a few times a day rather than on every read of
|
||||||
|
+ // the account list.
|
||||||
|
+ expires: Utc::now() + Duration::hours(6),
|
||||||
|
+ active: true,
|
||||||
|
+ })
|
||||||
|
+ }
|
||||||
|
+
|
||||||
|
+ /// Renews an Ely.by token pair, returning whether it is now usable.
|
||||||
|
+ ///
|
||||||
|
+ /// Ely.by refuses a pair that has been invalidated elsewhere - signing in
|
||||||
|
+ /// from another launcher does that - and there is no way back from it
|
||||||
|
+ /// without the password, so the caller is told rather than the launch
|
||||||
|
+ /// failing on its own later.
|
||||||
|
+ async fn refresh_ely(&mut self) -> crate::Result<bool> {
|
||||||
|
+ let Some(client_token) = self.ely_client_token() else {
|
||||||
|
+ return Ok(false);
|
||||||
|
+ };
|
||||||
|
+
|
||||||
|
+ let response = INSECURE_REQWEST_CLIENT
|
||||||
|
+ .post(format!("{ELY_AUTHSERVER}/auth/refresh"))
|
||||||
|
+ .json(&json!({
|
||||||
|
+ "accessToken": &self.access_token,
|
||||||
|
+ "clientToken": client_token,
|
||||||
|
+ "requestUser": false,
|
||||||
|
+ }))
|
||||||
|
+ .send()
|
||||||
|
+ .await;
|
||||||
|
+
|
||||||
|
+ let response = match response {
|
||||||
|
+ Ok(response) => response,
|
||||||
|
+ // Ely.by being unreachable says nothing about the token. Leave it
|
||||||
|
+ // alone: an offline launch with a still-valid token works.
|
||||||
|
+ Err(error) => {
|
||||||
|
+ tracing::warn!("Could not reach Ely.by to refresh: {error}");
|
||||||
|
+ return Ok(true);
|
||||||
|
+ }
|
||||||
|
+ };
|
||||||
|
+
|
||||||
|
+ if response.status().is_server_error() {
|
||||||
|
+ return Ok(true);
|
||||||
|
+ }
|
||||||
|
+
|
||||||
|
+ if !response.status().is_success() {
|
||||||
|
+ return Ok(false);
|
||||||
|
+ }
|
||||||
|
+
|
||||||
|
+ let auth = response.json::<ElyAuthResponse>().await?;
|
||||||
|
+ self.access_token = auth.access_token;
|
||||||
|
+ self.refresh_token =
|
||||||
|
+ format!("{ELY_REFRESH_TOKEN_PREFIX}{}", auth.client_token);
|
||||||
|
+ self.offline_profile = MinecraftProfile {
|
||||||
|
+ id: parse_ely_uuid(&auth.selected_profile.id)?,
|
||||||
|
+ name: auth.selected_profile.name,
|
||||||
|
+ ..MinecraftProfile::default()
|
||||||
|
+ };
|
||||||
|
+
|
||||||
|
+ Ok(true)
|
||||||
|
+ }
|
||||||
|
+
|
||||||
|
+ /// Whether Ely.by still accepts this account's access token.
|
||||||
|
+ async fn ely_token_is_valid(&self) -> bool {
|
||||||
|
+ let response = INSECURE_REQWEST_CLIENT
|
||||||
|
+ .post(format!("{ELY_AUTHSERVER}/auth/validate"))
|
||||||
|
+ .json(&json!({ "accessToken": &self.access_token }))
|
||||||
|
+ .send()
|
||||||
|
+ .await;
|
||||||
|
+
|
||||||
|
+ match response {
|
||||||
|
+ Ok(response) => response.status().is_success(),
|
||||||
|
+ // Unreachable is not invalid; see `refresh_ely`.
|
||||||
|
+ Err(_) => true,
|
||||||
|
+ }
|
||||||
|
+ }
|
||||||
|
+
|
||||||
|
/// Refreshes the authentication tokens for this user if they are expired, or
|
||||||
|
/// very close to expiration.
|
||||||
|
async fn refresh(
|
||||||
|
@@ -340,6 +530,22 @@ impl Credentials {
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
|
||||||
|
+ // Ely.by issues its own tokens and renews them at its own endpoint,
|
||||||
|
+ // so Microsoft is not involved at any point below.
|
||||||
|
+ if self.is_ely() {
|
||||||
|
+ if !self.ely_token_is_valid().await && !self.refresh_ely().await? {
|
||||||
|
+ return Err(crate::ErrorKind::OtherError(
|
||||||
|
+ "Ely.by no longer accepts this account's session. Sign in again."
|
||||||
|
+ .to_string(),
|
||||||
|
+ )
|
||||||
|
+ .into());
|
||||||
|
+ }
|
||||||
|
+
|
||||||
|
+ self.expires = Utc::now() + Duration::hours(6);
|
||||||
|
+ self.upsert(exec).await?;
|
||||||
|
+ return Ok(());
|
||||||
|
+ }
|
||||||
|
+
|
||||||
|
let oauth_token = oauth_refresh(&self.refresh_token).await?;
|
||||||
|
let (pair, current_date) =
|
||||||
|
DeviceTokenPair::refresh_and_get_device_token(
|
||||||
|
@@ -413,9 +619,10 @@ impl Credentials {
|
||||||
|
&self,
|
||||||
|
cache_intent: OnlineProfileCacheIntent,
|
||||||
|
) -> Option<Arc<MinecraftProfile>> {
|
||||||
|
- // Offline accounts have no Mojang profile, so skip the request that
|
||||||
|
- // would only ever fail and fall back to the offline profile.
|
||||||
|
- if self.is_offline() {
|
||||||
|
+ // Neither offline nor Ely.by accounts have a Mojang profile, so skip
|
||||||
|
+ // the request that would only ever fail and fall back to the profile
|
||||||
|
+ // recorded locally, which already holds the right id and name.
|
||||||
|
+ if self.is_offline() || self.is_ely() {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
|
||||||
|
diff --git a/packages/app-lib/src/util/authlib_injector.rs b/packages/app-lib/src/util/authlib_injector.rs
|
||||||
|
new file mode 100644
|
||||||
|
index 0000000..dc099f9
|
||||||
|
--- /dev/null
|
||||||
|
+++ b/packages/app-lib/src/util/authlib_injector.rs
|
||||||
|
@@ -0,0 +1,77 @@
|
||||||
|
+//! Downloads and caches the authlib-injector Java agent.
|
||||||
|
+//!
|
||||||
|
+//! Minecraft asks Mojang who a player is. An Ely.by account is not a Mojang
|
||||||
|
+//! account, so the game has to be told to ask Ely.by instead, and there is no
|
||||||
|
+//! switch for that: authlib-injector is a Java agent that rewrites the calls
|
||||||
|
+//! on the way out. Without it an Ely.by account cannot start the game at all.
|
||||||
|
+
|
||||||
|
+use std::path::PathBuf;
|
||||||
|
+
|
||||||
|
+use crate::state::DirectoryInfo;
|
||||||
|
+use crate::util::fetch::REQWEST_CLIENT;
|
||||||
|
+use crate::util::io;
|
||||||
|
+
|
||||||
|
+/// The agent's own distribution metadata.
|
||||||
|
+const LATEST_URL: &str = "https://authlib-injector.yushi.moe/artifact/latest.json";
|
||||||
|
+
|
||||||
|
+#[derive(serde::Deserialize)]
|
||||||
|
+struct LatestArtifact {
|
||||||
|
+ download_url: String,
|
||||||
|
+}
|
||||||
|
+
|
||||||
|
+/// 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
|
||||||
|
+/// launcher version, so there is nothing to keep up to date, and reusing it
|
||||||
|
+/// means an Ely.by account still launches with no network at all.
|
||||||
|
+pub async fn get_authlib_injector(
|
||||||
|
+ directories: &DirectoryInfo,
|
||||||
|
+) -> crate::Result<PathBuf> {
|
||||||
|
+ let dir = directories.caches_dir().join("authlib-injector");
|
||||||
|
+ io::create_dir_all(&dir).await?;
|
||||||
|
+
|
||||||
|
+ let jar = dir.join("authlib-injector.jar");
|
||||||
|
+ if io::metadata(&jar).await.is_ok() {
|
||||||
|
+ return Ok(jar);
|
||||||
|
+ }
|
||||||
|
+
|
||||||
|
+ tracing::info!("Downloading authlib-injector for an Ely.by launch");
|
||||||
|
+
|
||||||
|
+ let latest = REQWEST_CLIENT
|
||||||
|
+ .get(LATEST_URL)
|
||||||
|
+ .send()
|
||||||
|
+ .await
|
||||||
|
+ .map_err(|error| {
|
||||||
|
+ crate::ErrorKind::OtherError(format!(
|
||||||
|
+ "Could not reach the authlib-injector distribution: {error}"
|
||||||
|
+ ))
|
||||||
|
+ })?
|
||||||
|
+ .json::<LatestArtifact>()
|
||||||
|
+ .await
|
||||||
|
+ .map_err(|error| {
|
||||||
|
+ crate::ErrorKind::OtherError(format!(
|
||||||
|
+ "Could not read the authlib-injector metadata: {error}"
|
||||||
|
+ ))
|
||||||
|
+ })?;
|
||||||
|
+
|
||||||
|
+ let bytes = REQWEST_CLIENT
|
||||||
|
+ .get(&latest.download_url)
|
||||||
|
+ .send()
|
||||||
|
+ .await
|
||||||
|
+ .map_err(|error| {
|
||||||
|
+ crate::ErrorKind::OtherError(format!(
|
||||||
|
+ "Could not download authlib-injector: {error}"
|
||||||
|
+ ))
|
||||||
|
+ })?
|
||||||
|
+ .bytes()
|
||||||
|
+ .await
|
||||||
|
+ .map_err(|error| {
|
||||||
|
+ crate::ErrorKind::OtherError(format!(
|
||||||
|
+ "Could not read the authlib-injector download: {error}"
|
||||||
|
+ ))
|
||||||
|
+ })?;
|
||||||
|
+
|
||||||
|
+ io::write(&jar, &bytes).await?;
|
||||||
|
+
|
||||||
|
+ Ok(jar)
|
||||||
|
+}
|
||||||
|
diff --git a/packages/app-lib/src/util/mod.rs b/packages/app-lib/src/util/mod.rs
|
||||||
|
index 7656b4a..d0e4d5d 100644
|
||||||
|
--- a/packages/app-lib/src/util/mod.rs
|
||||||
|
+++ b/packages/app-lib/src/util/mod.rs
|
||||||
|
@@ -1,4 +1,5 @@
|
||||||
|
//! Theseus utility functions
|
||||||
|
+pub mod authlib_injector;
|
||||||
|
pub mod fetch;
|
||||||
|
pub mod io;
|
||||||
|
pub mod jre;
|
||||||
@@ -50,6 +50,20 @@ check "the Tauri command is registered" \
|
|||||||
check "the frontend can reach it" \
|
check "the frontend can reach it" \
|
||||||
contains "$WORKTREE/apps/app-frontend/src/helpers/auth.js" "plugin:auth|login_offline"
|
contains "$WORKTREE/apps/app-frontend/src/helpers/auth.js" "plugin:auth|login_offline"
|
||||||
|
|
||||||
|
log "Ely.by accounts"
|
||||||
|
check "app-lib can sign in to Ely.by" \
|
||||||
|
contains "$WORKTREE/packages/app-lib/src/api/minecraft_auth.rs" 'pub async fn login_ely'
|
||||||
|
check "the Tauri command is registered" \
|
||||||
|
contains "$WORKTREE/apps/app/src/api/auth.rs" 'login_ely,'
|
||||||
|
check "authlib-injector is added at launch" \
|
||||||
|
contains "$WORKTREE/packages/app-lib/src/launcher/mod.rs" 'authlib_injector'
|
||||||
|
|
||||||
|
log "Sidebar and news"
|
||||||
|
check "Modrinth Servers is behind a flag" \
|
||||||
|
contains "$WORKTREE/apps/app-frontend/src/App.vue" "getFeatureFlag('show_hosting_in_sidebar')"
|
||||||
|
check "the news section can be collapsed" \
|
||||||
|
contains "$WORKTREE/apps/app-frontend/src/App.vue" 'setNewsCollapsed'
|
||||||
|
|
||||||
log "No telemetry in the sources"
|
log "No telemetry in the sources"
|
||||||
# Quoted, so that the module names being mentioned in a comment explaining why
|
# Quoted, so that the module names being mentioned in a comment explaining why
|
||||||
# they are gone does not count as importing them.
|
# they are gone does not count as importing them.
|
||||||
|
|||||||
Reference in new issue
Block a user