Skip to content

Commit 48c3b64

Browse files
AlexTMjugadorIMB11
authored andcommitted
feat(app): better external browser Modrinth login flow (#4033)
* fix(app-frontend): do not emit exceptions when no loaders are available * refactor(app): simplify Microsoft login code without functional changes * feat(app): external browser auth flow for Modrinth account login * chore: address Clippy lint * chore(app/oauth_utils): simplify `handle_reply` error handling according to review * chore(app-lib): simplify `Url` usage out of MC auth module
1 parent c916044 commit 48c3b64

File tree

20 files changed

+391
-147
lines changed

20 files changed

+391
-147
lines changed

Cargo.lock

Lines changed: 2 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,7 @@ heck = "0.5.0"
6767
hex = "0.4.3"
6868
hickory-resolver = "0.25.2"
6969
hmac = "0.12.1"
70+
hyper = "1.6.0"
7071
hyper-rustls = { version = "0.27.7", default-features = false, features = [
7172
"http1",
7273
"native-tokio",

apps/app-frontend/src/App.vue

Lines changed: 25 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -61,9 +61,10 @@ import { renderString } from '@modrinth/utils'
6161
import { useFetch } from '@/helpers/fetch.js'
6262
import { check } from '@tauri-apps/plugin-updater'
6363
import NavButton from '@/components/ui/NavButton.vue'
64-
import { get as getCreds, login, logout } from '@/helpers/mr_auth.js'
64+
import { cancelLogin, get as getCreds, login, logout } from '@/helpers/mr_auth.js'
6565
import { get_user } from '@/helpers/cache.js'
6666
import AppSettingsModal from '@/components/ui/modal/AppSettingsModal.vue'
67+
import AuthGrantFlowWaitModal from '@/components/ui/modal/AuthGrantFlowWaitModal.vue'
6768
import PromotionWrapper from '@/components/ui/PromotionWrapper.vue'
6869
import { hide_ads_window, init_ads_window } from '@/helpers/ads.js'
6970
import FriendsList from '@/components/ui/friends/FriendsList.vue'
@@ -263,6 +264,8 @@ const incompatibilityWarningModal = ref()
263264
264265
const credentials = ref()
265266
267+
const modrinthLoginFlowWaitModal = ref()
268+
266269
async function fetchCredentials() {
267270
const creds = await getCreds().catch(handleError)
268271
if (creds && creds.user_id) {
@@ -272,8 +275,24 @@ async function fetchCredentials() {
272275
}
273276
274277
async function signIn() {
275-
await login().catch(handleError)
276-
await fetchCredentials()
278+
modrinthLoginFlowWaitModal.value.show()
279+
280+
try {
281+
await login()
282+
await fetchCredentials()
283+
} catch (error) {
284+
if (
285+
typeof error === 'object' &&
286+
typeof error['message'] === 'string' &&
287+
error.message.includes('Login canceled')
288+
) {
289+
// Not really an error due to being a result of user interaction, show nothing
290+
} else {
291+
handleError(error)
292+
}
293+
} finally {
294+
modrinthLoginFlowWaitModal.value.hide()
295+
}
277296
}
278297
279298
async function logOut() {
@@ -402,6 +421,9 @@ function handleAuxClick(e) {
402421
<Suspense>
403422
<AppSettingsModal ref="settingsModal" />
404423
</Suspense>
424+
<Suspense>
425+
<AuthGrantFlowWaitModal ref="modrinthLoginFlowWaitModal" @flow-cancel="cancelLogin" />
426+
</Suspense>
405427
<Suspense>
406428
<InstanceCreationModal ref="installationModal" />
407429
</Suspense>

apps/app-frontend/src/components/ui/InstanceCreationModal.vue

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -305,12 +305,16 @@ const [
305305
get_game_versions().then(shallowRef).catch(handleError),
306306
get_loaders()
307307
.then((value) =>
308-
value
309-
.filter((item) => item.supported_project_types.includes('modpack'))
310-
.map((item) => item.name.toLowerCase()),
308+
ref(
309+
value
310+
.filter((item) => item.supported_project_types.includes('modpack'))
311+
.map((item) => item.name.toLowerCase()),
312+
),
311313
)
312-
.then(ref)
313-
.catch(handleError),
314+
.catch((err) => {
315+
handleError(err)
316+
return ref([])
317+
}),
314318
])
315319
loaders.value.unshift('vanilla')
316320
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
<script setup lang="ts">
2+
import { LogInIcon, SpinnerIcon } from '@modrinth/assets'
3+
import { ref } from 'vue'
4+
import ModalWrapper from '@/components/ui/modal/ModalWrapper.vue'
5+
6+
defineProps({
7+
onFlowCancel: {
8+
type: Function,
9+
default() {
10+
return async () => {}
11+
},
12+
},
13+
})
14+
15+
const modal = ref()
16+
17+
function show() {
18+
modal.value.show()
19+
}
20+
21+
function hide() {
22+
modal.value.hide()
23+
}
24+
25+
defineExpose({ show, hide })
26+
</script>
27+
<template>
28+
<ModalWrapper ref="modal" @hide="onFlowCancel">
29+
<template #title>
30+
<span class="items-center gap-2 text-lg font-extrabold text-contrast">
31+
<LogInIcon /> Sign in
32+
</span>
33+
</template>
34+
35+
<div class="flex justify-center gap-2">
36+
<SpinnerIcon class="w-12 h-12 animate-spin" />
37+
</div>
38+
<p class="text-sm text-secondary">
39+
Please sign in at the browser window that just opened to continue.
40+
</p>
41+
</ModalWrapper>
42+
</template>

apps/app-frontend/src/helpers/mr_auth.js

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,3 +16,7 @@ export async function logout() {
1616
export async function get() {
1717
return await invoke('plugin:mr-auth|get')
1818
}
19+
20+
export async function cancelLogin() {
21+
return await invoke('plugin:mr-auth|cancel_modrinth_login')
22+
}

apps/app-playground/src/main.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ pub async fn authenticate_run() -> theseus::Result<Credentials> {
1515
println!("A browser window will now open, follow the login flow there.");
1616
let login = minecraft_auth::begin_login().await?;
1717

18-
println!("Open URL {} in a browser", login.redirect_uri.as_str());
18+
println!("Open URL {} in a browser", login.auth_request_uri.as_str());
1919

2020
println!("Please enter URL code: ");
2121
let mut input = String::new();

apps/app/Cargo.toml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,8 @@ thiserror.workspace = true
3131
daedalus.workspace = true
3232
chrono.workspace = true
3333
either.workspace = true
34+
hyper = { workspace = true, features = ["server"] }
35+
hyper-util.workspace = true
3436

3537
url.workspace = true
3638
urlencoding.workspace = true

apps/app/build.rs

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -120,7 +120,12 @@ fn main() {
120120
.plugin(
121121
"mr-auth",
122122
InlinedPlugin::new()
123-
.commands(&["modrinth_login", "logout", "get"])
123+
.commands(&[
124+
"modrinth_login",
125+
"logout",
126+
"get",
127+
"cancel_modrinth_login",
128+
])
124129
.default_permission(
125130
DefaultPermissionRule::AllowAllCommands,
126131
),

apps/app/src/api/auth.rs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@ pub async fn login<R: Runtime>(
3333
let window = tauri::WebviewWindowBuilder::new(
3434
&app,
3535
"signin",
36-
tauri::WebviewUrl::External(flow.redirect_uri.parse().map_err(
36+
tauri::WebviewUrl::External(flow.auth_request_uri.parse().map_err(
3737
|_| {
3838
theseus::ErrorKind::OtherError(
3939
"Error parsing auth redirect URL".to_string(),
@@ -77,6 +77,7 @@ pub async fn login<R: Runtime>(
7777
window.close()?;
7878
Ok(None)
7979
}
80+
8081
#[tauri::command]
8182
pub async fn remove_user(user: uuid::Uuid) -> Result<()> {
8283
Ok(minecraft_auth::remove_user(user).await?)

0 commit comments

Comments
 (0)