parent
a35dd67b77
commit
47e28d24c8
6
Cargo.lock
generated
6
Cargo.lock
generated
@ -4609,7 +4609,7 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "theseus"
|
name = "theseus"
|
||||||
version = "0.3.1"
|
version = "0.4.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"async-recursion",
|
"async-recursion",
|
||||||
"async-tungstenite",
|
"async-tungstenite",
|
||||||
@ -4654,7 +4654,7 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "theseus_cli"
|
name = "theseus_cli"
|
||||||
version = "0.3.1"
|
version = "0.4.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"argh",
|
"argh",
|
||||||
"color-eyre",
|
"color-eyre",
|
||||||
@ -4681,7 +4681,7 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "theseus_gui"
|
name = "theseus_gui"
|
||||||
version = "0.3.1"
|
version = "0.4.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"chrono",
|
"chrono",
|
||||||
"cocoa",
|
"cocoa",
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "theseus"
|
name = "theseus"
|
||||||
version = "0.3.1"
|
version = "0.4.0"
|
||||||
authors = ["Jai A <jaiagr+gpg@pm.me>"]
|
authors = ["Jai A <jaiagr+gpg@pm.me>"]
|
||||||
edition = "2018"
|
edition = "2018"
|
||||||
|
|
||||||
|
|||||||
@ -20,7 +20,8 @@ pub async fn authenticate_begin_flow() -> crate::Result<url::Url> {
|
|||||||
/// This completes the authentication flow quasi-synchronously, returning the credentials
|
/// This completes the authentication flow quasi-synchronously, returning the credentials
|
||||||
/// This can be used in conjunction with 'authenticate_begin_flow'
|
/// This can be used in conjunction with 'authenticate_begin_flow'
|
||||||
/// to call authenticate and call the flow from the frontend.
|
/// to call authenticate and call the flow from the frontend.
|
||||||
pub async fn authenticate_await_complete_flow() -> crate::Result<Credentials> {
|
pub async fn authenticate_await_complete_flow(
|
||||||
|
) -> crate::Result<(Credentials, Option<String>)> {
|
||||||
let credentials = AuthTask::await_auth_completion().await?;
|
let credentials = AuthTask::await_auth_completion().await?;
|
||||||
Ok(credentials)
|
Ok(credentials)
|
||||||
}
|
}
|
||||||
@ -38,7 +39,7 @@ pub async fn cancel_flow() -> crate::Result<()> {
|
|||||||
#[theseus_macros::debug_pin]
|
#[theseus_macros::debug_pin]
|
||||||
pub async fn authenticate(
|
pub async fn authenticate(
|
||||||
browser_url: oneshot::Sender<url::Url>,
|
browser_url: oneshot::Sender<url::Url>,
|
||||||
) -> crate::Result<Credentials> {
|
) -> crate::Result<(Credentials, Option<String>)> {
|
||||||
let mut flow = inner::HydraAuthFlow::new().await?;
|
let mut flow = inner::HydraAuthFlow::new().await?;
|
||||||
let state = State::get().await?;
|
let state = State::get().await?;
|
||||||
|
|
||||||
@ -52,12 +53,12 @@ pub async fn authenticate(
|
|||||||
let credentials = flow.extract_credentials(&state.fetch_semaphore).await?;
|
let credentials = flow.extract_credentials(&state.fetch_semaphore).await?;
|
||||||
{
|
{
|
||||||
let mut users = state.users.write().await;
|
let mut users = state.users.write().await;
|
||||||
users.insert(&credentials).await?;
|
users.insert(&credentials.0).await?;
|
||||||
}
|
}
|
||||||
|
|
||||||
if state.settings.read().await.default_user.is_none() {
|
if state.settings.read().await.default_user.is_none() {
|
||||||
let mut settings = state.settings.write().await;
|
let mut settings = state.settings.write().await;
|
||||||
settings.default_user = Some(credentials.id);
|
settings.default_user = Some(credentials.0.id);
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(credentials)
|
Ok(credentials)
|
||||||
@ -79,8 +80,17 @@ pub async fn refresh(user: uuid::Uuid) -> crate::Result<Credentials> {
|
|||||||
})?;
|
})?;
|
||||||
|
|
||||||
let fetch_semaphore = &state.fetch_semaphore;
|
let fetch_semaphore = &state.fetch_semaphore;
|
||||||
if Utc::now() > credentials.expires {
|
if Utc::now() > credentials.expires
|
||||||
inner::refresh_credentials(&mut credentials, fetch_semaphore).await?;
|
&& inner::refresh_credentials(&mut credentials, fetch_semaphore)
|
||||||
|
.await
|
||||||
|
.is_err()
|
||||||
|
{
|
||||||
|
users.remove(credentials.id).await?;
|
||||||
|
|
||||||
|
return Err(crate::ErrorKind::OtherError(
|
||||||
|
"Please re-authenticate with your Minecraft account!".to_string(),
|
||||||
|
)
|
||||||
|
.as_error());
|
||||||
}
|
}
|
||||||
users.insert(&credentials).await?;
|
users.insert(&credentials).await?;
|
||||||
|
|
||||||
|
|||||||
@ -4,6 +4,7 @@ use serde::Deserialize;
|
|||||||
use std::path::PathBuf;
|
use std::path::PathBuf;
|
||||||
|
|
||||||
use crate::event::emit::{emit_loading, init_loading};
|
use crate::event::emit::{emit_loading, init_loading};
|
||||||
|
use crate::state::CredentialsStore;
|
||||||
use crate::util::fetch::{fetch_advanced, fetch_json};
|
use crate::util::fetch::{fetch_advanced, fetch_json};
|
||||||
use crate::util::io;
|
use crate::util::io;
|
||||||
use crate::util::jre::extract_java_majorminor_version;
|
use crate::util::jre::extract_java_majorminor_version;
|
||||||
@ -97,6 +98,7 @@ pub async fn auto_install_java(java_version: u32) -> crate::Result<PathBuf> {
|
|||||||
None,
|
None,
|
||||||
None,
|
None,
|
||||||
&state.fetch_semaphore,
|
&state.fetch_semaphore,
|
||||||
|
&CredentialsStore(None),
|
||||||
).await?;
|
).await?;
|
||||||
emit_loading(&loading_bar, 10.0, Some("Downloading java version")).await?;
|
emit_loading(&loading_bar, 10.0, Some("Downloading java version")).await?;
|
||||||
|
|
||||||
@ -109,6 +111,7 @@ pub async fn auto_install_java(java_version: u32) -> crate::Result<PathBuf> {
|
|||||||
None,
|
None,
|
||||||
Some((&loading_bar, 80.0)),
|
Some((&loading_bar, 80.0)),
|
||||||
&state.fetch_semaphore,
|
&state.fetch_semaphore,
|
||||||
|
&CredentialsStore(None),
|
||||||
)
|
)
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
|
|||||||
@ -4,6 +4,7 @@ pub mod handler;
|
|||||||
pub mod jre;
|
pub mod jre;
|
||||||
pub mod logs;
|
pub mod logs;
|
||||||
pub mod metadata;
|
pub mod metadata;
|
||||||
|
pub mod mr_auth;
|
||||||
pub mod pack;
|
pub mod pack;
|
||||||
pub mod process;
|
pub mod process;
|
||||||
pub mod profile;
|
pub mod profile;
|
||||||
@ -14,9 +15,9 @@ pub mod tags;
|
|||||||
pub mod data {
|
pub mod data {
|
||||||
pub use crate::state::{
|
pub use crate::state::{
|
||||||
DirectoryInfo, Hooks, JavaSettings, LinkedData, MemorySettings,
|
DirectoryInfo, Hooks, JavaSettings, LinkedData, MemorySettings,
|
||||||
ModLoader, ModrinthProject, ModrinthTeamMember, ModrinthUser,
|
ModLoader, ModrinthCredentials, ModrinthCredentialsResult,
|
||||||
ModrinthVersion, ProfileMetadata, ProjectMetadata, Settings, Theme,
|
ModrinthProject, ModrinthTeamMember, ModrinthUser, ModrinthVersion,
|
||||||
WindowSize,
|
ProfileMetadata, ProjectMetadata, Settings, Theme, WindowSize,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
157
theseus/src/api/mr_auth.rs
Normal file
157
theseus/src/api/mr_auth.rs
Normal file
@ -0,0 +1,157 @@
|
|||||||
|
use crate::state::{
|
||||||
|
ModrinthAuthFlow, ModrinthCredentials, ModrinthCredentialsResult,
|
||||||
|
};
|
||||||
|
use crate::ErrorKind;
|
||||||
|
|
||||||
|
#[tracing::instrument]
|
||||||
|
pub async fn authenticate_begin_flow(provider: &str) -> crate::Result<String> {
|
||||||
|
let state = crate::State::get().await?;
|
||||||
|
|
||||||
|
let mut flow = ModrinthAuthFlow::new(provider).await?;
|
||||||
|
let url = flow.prepare_login_url().await?;
|
||||||
|
|
||||||
|
let mut write = state.modrinth_auth_flow.write().await;
|
||||||
|
*write = Some(flow);
|
||||||
|
|
||||||
|
Ok(url)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tracing::instrument]
|
||||||
|
pub async fn authenticate_await_complete_flow(
|
||||||
|
) -> crate::Result<ModrinthCredentialsResult> {
|
||||||
|
let state = crate::State::get().await?;
|
||||||
|
|
||||||
|
let mut write = state.modrinth_auth_flow.write().await;
|
||||||
|
if let Some(ref mut flow) = *write {
|
||||||
|
let creds = flow.extract_credentials(&state.fetch_semaphore).await?;
|
||||||
|
|
||||||
|
if let ModrinthCredentialsResult::Credentials(creds) = &creds {
|
||||||
|
let mut write = state.credentials.write().await;
|
||||||
|
write.login(creds.clone()).await?;
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(creds)
|
||||||
|
} else {
|
||||||
|
Err(ErrorKind::OtherError(
|
||||||
|
"No active Modrinth authenication flow!".to_string(),
|
||||||
|
)
|
||||||
|
.into())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tracing::instrument]
|
||||||
|
pub async fn cancel_flow() -> crate::Result<()> {
|
||||||
|
let state = crate::State::get().await?;
|
||||||
|
let mut write = state.modrinth_auth_flow.write().await;
|
||||||
|
if let Some(ref mut flow) = *write {
|
||||||
|
flow.close().await?;
|
||||||
|
}
|
||||||
|
*write = None;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn login_password(
|
||||||
|
username: &str,
|
||||||
|
password: &str,
|
||||||
|
challenge: &str,
|
||||||
|
) -> crate::Result<ModrinthCredentialsResult> {
|
||||||
|
let state = crate::State::get().await?;
|
||||||
|
let creds = crate::state::login_password(
|
||||||
|
username,
|
||||||
|
password,
|
||||||
|
challenge,
|
||||||
|
&state.fetch_semaphore,
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
if let ModrinthCredentialsResult::Credentials(creds) = &creds {
|
||||||
|
let mut write = state.credentials.write().await;
|
||||||
|
write.login(creds.clone()).await?;
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(creds)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tracing::instrument]
|
||||||
|
pub async fn login_2fa(
|
||||||
|
code: &str,
|
||||||
|
flow: &str,
|
||||||
|
) -> crate::Result<ModrinthCredentials> {
|
||||||
|
let state = crate::State::get().await?;
|
||||||
|
let creds =
|
||||||
|
crate::state::login_2fa(code, flow, &state.fetch_semaphore).await?;
|
||||||
|
|
||||||
|
let mut write = state.credentials.write().await;
|
||||||
|
write.login(creds.clone()).await?;
|
||||||
|
|
||||||
|
Ok(creds)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tracing::instrument]
|
||||||
|
pub async fn login_minecraft(
|
||||||
|
flow: &str,
|
||||||
|
) -> crate::Result<ModrinthCredentialsResult> {
|
||||||
|
let state = crate::State::get().await?;
|
||||||
|
let creds =
|
||||||
|
crate::state::login_minecraft(flow, &state.fetch_semaphore).await?;
|
||||||
|
|
||||||
|
if let ModrinthCredentialsResult::Credentials(creds) = &creds {
|
||||||
|
let mut write = state.credentials.write().await;
|
||||||
|
write.login(creds.clone()).await?;
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(creds)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tracing::instrument]
|
||||||
|
pub async fn create_account(
|
||||||
|
username: &str,
|
||||||
|
email: &str,
|
||||||
|
password: &str,
|
||||||
|
challenge: &str,
|
||||||
|
sign_up_newsletter: bool,
|
||||||
|
) -> crate::Result<ModrinthCredentials> {
|
||||||
|
let state = crate::State::get().await?;
|
||||||
|
let creds = crate::state::create_account(
|
||||||
|
username,
|
||||||
|
email,
|
||||||
|
password,
|
||||||
|
challenge,
|
||||||
|
sign_up_newsletter,
|
||||||
|
&state.fetch_semaphore,
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
let mut write = state.credentials.write().await;
|
||||||
|
write.login(creds.clone()).await?;
|
||||||
|
|
||||||
|
Ok(creds)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tracing::instrument]
|
||||||
|
pub async fn refresh() -> crate::Result<()> {
|
||||||
|
let state = crate::State::get().await?;
|
||||||
|
|
||||||
|
let mut write = state.credentials.write().await;
|
||||||
|
crate::state::refresh_credentials(&mut write, &state.fetch_semaphore)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tracing::instrument]
|
||||||
|
pub async fn logout() -> crate::Result<()> {
|
||||||
|
let state = crate::State::get().await?;
|
||||||
|
let mut write = state.credentials.write().await;
|
||||||
|
write.logout().await?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tracing::instrument]
|
||||||
|
pub async fn get_credentials() -> crate::Result<Option<ModrinthCredentials>> {
|
||||||
|
let state = crate::State::get().await?;
|
||||||
|
let read = state.credentials.read().await;
|
||||||
|
|
||||||
|
Ok(read.0.clone())
|
||||||
|
}
|
||||||
@ -2,6 +2,7 @@ use std::path::PathBuf;
|
|||||||
|
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
|
use crate::state::CredentialsStore;
|
||||||
use crate::{
|
use crate::{
|
||||||
prelude::{ModLoader, ProfilePathId},
|
prelude::{ModLoader, ProfilePathId},
|
||||||
state::ProfileInstallStage,
|
state::ProfileInstallStage,
|
||||||
@ -90,8 +91,13 @@ pub async fn import_curseforge(
|
|||||||
thumbnail_url: Some(thumbnail_url),
|
thumbnail_url: Some(thumbnail_url),
|
||||||
}) = minecraft_instance.installed_modpack.clone()
|
}) = minecraft_instance.installed_modpack.clone()
|
||||||
{
|
{
|
||||||
let icon_bytes =
|
let icon_bytes = fetch(
|
||||||
fetch(&thumbnail_url, None, &state.fetch_semaphore).await?;
|
&thumbnail_url,
|
||||||
|
None,
|
||||||
|
&state.fetch_semaphore,
|
||||||
|
&CredentialsStore(None),
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
let filename = thumbnail_url.rsplit('/').last();
|
let filename = thumbnail_url.rsplit('/').last();
|
||||||
if let Some(filename) = filename {
|
if let Some(filename) = filename {
|
||||||
icon = Some(
|
icon = Some(
|
||||||
|
|||||||
@ -192,12 +192,14 @@ pub async fn generate_pack_from_version_id(
|
|||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
emit_loading(&loading_bar, 0.0, Some("Fetching version")).await?;
|
emit_loading(&loading_bar, 0.0, Some("Fetching version")).await?;
|
||||||
|
let creds = state.credentials.read().await;
|
||||||
let version: ModrinthVersion = fetch_json(
|
let version: ModrinthVersion = fetch_json(
|
||||||
Method::GET,
|
Method::GET,
|
||||||
&format!("{}version/{}", MODRINTH_API_URL, version_id),
|
&format!("{}version/{}", MODRINTH_API_URL, version_id),
|
||||||
None,
|
None,
|
||||||
None,
|
None,
|
||||||
&state.fetch_semaphore,
|
&state.fetch_semaphore,
|
||||||
|
&creds,
|
||||||
)
|
)
|
||||||
.await?;
|
.await?;
|
||||||
emit_loading(&loading_bar, 10.0, None).await?;
|
emit_loading(&loading_bar, 10.0, None).await?;
|
||||||
@ -225,6 +227,7 @@ pub async fn generate_pack_from_version_id(
|
|||||||
None,
|
None,
|
||||||
Some((&loading_bar, 70.0)),
|
Some((&loading_bar, 70.0)),
|
||||||
&state.fetch_semaphore,
|
&state.fetch_semaphore,
|
||||||
|
&creds,
|
||||||
)
|
)
|
||||||
.await?;
|
.await?;
|
||||||
emit_loading(&loading_bar, 0.0, Some("Fetching project metadata")).await?;
|
emit_loading(&loading_bar, 0.0, Some("Fetching project metadata")).await?;
|
||||||
@ -235,13 +238,16 @@ pub async fn generate_pack_from_version_id(
|
|||||||
None,
|
None,
|
||||||
None,
|
None,
|
||||||
&state.fetch_semaphore,
|
&state.fetch_semaphore,
|
||||||
|
&creds,
|
||||||
)
|
)
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
emit_loading(&loading_bar, 10.0, Some("Retrieving icon")).await?;
|
emit_loading(&loading_bar, 10.0, Some("Retrieving icon")).await?;
|
||||||
let icon = if let Some(icon_url) = project.icon_url {
|
let icon = if let Some(icon_url) = project.icon_url {
|
||||||
let state = State::get().await?;
|
let state = State::get().await?;
|
||||||
let icon_bytes = fetch(&icon_url, None, &state.fetch_semaphore).await?;
|
let icon_bytes =
|
||||||
|
fetch(&icon_url, None, &state.fetch_semaphore, &creds).await?;
|
||||||
|
drop(creds);
|
||||||
|
|
||||||
let filename = icon_url.rsplit('/').next();
|
let filename = icon_url.rsplit('/').next();
|
||||||
|
|
||||||
|
|||||||
@ -168,6 +168,7 @@ pub async fn install_zipped_mrpack_files(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let creds = state.credentials.read().await;
|
||||||
let file = fetch_mirrors(
|
let file = fetch_mirrors(
|
||||||
&project
|
&project
|
||||||
.downloads
|
.downloads
|
||||||
@ -176,8 +177,10 @@ pub async fn install_zipped_mrpack_files(
|
|||||||
.collect::<Vec<&str>>(),
|
.collect::<Vec<&str>>(),
|
||||||
project.hashes.get(&PackFileHash::Sha1).map(|x| &**x),
|
project.hashes.get(&PackFileHash::Sha1).map(|x| &**x),
|
||||||
&state.fetch_semaphore,
|
&state.fetch_semaphore,
|
||||||
|
&creds,
|
||||||
)
|
)
|
||||||
.await?;
|
.await?;
|
||||||
|
drop(creds);
|
||||||
|
|
||||||
let path =
|
let path =
|
||||||
std::path::Path::new(&project.path).components().next();
|
std::path::Path::new(&project.path).components().next();
|
||||||
|
|||||||
@ -1,4 +1,6 @@
|
|||||||
//! Authentication flow based on Hydra
|
//! Authentication flow based on Hydra
|
||||||
|
use crate::config::MODRINTH_API_URL;
|
||||||
|
use crate::state::CredentialsStore;
|
||||||
use crate::util::fetch::{fetch_advanced, fetch_json, FetchSemaphore};
|
use crate::util::fetch::{fetch_advanced, fetch_json, FetchSemaphore};
|
||||||
use async_tungstenite as ws;
|
use async_tungstenite as ws;
|
||||||
use chrono::{prelude::*, Duration};
|
use chrono::{prelude::*, Duration};
|
||||||
@ -10,7 +12,7 @@ use url::Url;
|
|||||||
|
|
||||||
lazy_static! {
|
lazy_static! {
|
||||||
static ref HYDRA_URL: Url =
|
static ref HYDRA_URL: Url =
|
||||||
Url::parse("https://staging-api.modrinth.com/v2/auth/minecraft/")
|
Url::parse(&format!("{MODRINTH_API_URL}auth/minecraft/"))
|
||||||
.expect("Hydra URL parse failed");
|
.expect("Hydra URL parse failed");
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -40,7 +42,7 @@ struct TokenJSON {
|
|||||||
token: String,
|
token: String,
|
||||||
refresh_token: String,
|
refresh_token: String,
|
||||||
expires_after: u32,
|
expires_after: u32,
|
||||||
flow: String,
|
flow: Option<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Deserialize)]
|
#[derive(Deserialize)]
|
||||||
@ -68,7 +70,7 @@ pub struct HydraAuthFlow<S: AsyncRead + AsyncWrite + Unpin> {
|
|||||||
impl HydraAuthFlow<ws::tokio::ConnectStream> {
|
impl HydraAuthFlow<ws::tokio::ConnectStream> {
|
||||||
pub async fn new() -> crate::Result<Self> {
|
pub async fn new() -> crate::Result<Self> {
|
||||||
let (socket, _) = ws::tokio::connect_async(
|
let (socket, _) = ws::tokio::connect_async(
|
||||||
"wss://staging-api.modrinth.com/v2/auth/minecraft/ws",
|
"wss://api.modrinth.com/v2/auth/minecraft/ws",
|
||||||
)
|
)
|
||||||
.await?;
|
.await?;
|
||||||
Ok(Self { socket })
|
Ok(Self { socket })
|
||||||
@ -96,7 +98,7 @@ impl HydraAuthFlow<ws::tokio::ConnectStream> {
|
|||||||
pub async fn extract_credentials(
|
pub async fn extract_credentials(
|
||||||
&mut self,
|
&mut self,
|
||||||
semaphore: &FetchSemaphore,
|
semaphore: &FetchSemaphore,
|
||||||
) -> crate::Result<Credentials> {
|
) -> crate::Result<(Credentials, Option<String>)> {
|
||||||
// Minecraft bearer token
|
// Minecraft bearer token
|
||||||
let token_resp = self
|
let token_resp = self
|
||||||
.socket
|
.socket
|
||||||
@ -117,14 +119,17 @@ impl HydraAuthFlow<ws::tokio::ConnectStream> {
|
|||||||
let info = fetch_info(&token.token, semaphore).await?;
|
let info = fetch_info(&token.token, semaphore).await?;
|
||||||
|
|
||||||
// Return structure from response
|
// Return structure from response
|
||||||
Ok(Credentials {
|
Ok((
|
||||||
username: info.name,
|
Credentials {
|
||||||
id: info.id,
|
username: info.name,
|
||||||
refresh_token: token.refresh_token,
|
id: info.id,
|
||||||
access_token: token.token,
|
refresh_token: token.refresh_token,
|
||||||
expires,
|
access_token: token.token,
|
||||||
_ctor_scope: std::marker::PhantomData,
|
expires,
|
||||||
})
|
_ctor_scope: std::marker::PhantomData,
|
||||||
|
},
|
||||||
|
token.flow,
|
||||||
|
))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -134,10 +139,11 @@ pub async fn refresh_credentials(
|
|||||||
) -> crate::Result<()> {
|
) -> crate::Result<()> {
|
||||||
let resp = fetch_json::<TokenJSON>(
|
let resp = fetch_json::<TokenJSON>(
|
||||||
Method::POST,
|
Method::POST,
|
||||||
"https://staging-api.modrinth.com/v2/auth/minecraft/refresh",
|
&format!("{MODRINTH_API_URL}auth/minecraft/refresh"),
|
||||||
None,
|
None,
|
||||||
Some(serde_json::json!({ "refresh_token": credentials.refresh_token })),
|
Some(serde_json::json!({ "refresh_token": credentials.refresh_token })),
|
||||||
semaphore,
|
semaphore,
|
||||||
|
&CredentialsStore(None),
|
||||||
)
|
)
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
@ -162,6 +168,7 @@ async fn fetch_info(
|
|||||||
Some(("Authorization", &format!("Bearer {token}"))),
|
Some(("Authorization", &format!("Bearer {token}"))),
|
||||||
None,
|
None,
|
||||||
semaphore,
|
semaphore,
|
||||||
|
&CredentialsStore(None),
|
||||||
)
|
)
|
||||||
.await?;
|
.await?;
|
||||||
let value = serde_json::from_slice(&result)?;
|
let value = serde_json::from_slice(&result)?;
|
||||||
|
|||||||
@ -1,5 +1,6 @@
|
|||||||
//! Downloader for Minecraft data
|
//! Downloader for Minecraft data
|
||||||
|
|
||||||
|
use crate::state::CredentialsStore;
|
||||||
use crate::{
|
use crate::{
|
||||||
event::{
|
event::{
|
||||||
emit::{emit_loading, loading_try_for_each_concurrent},
|
emit::{emit_loading, loading_try_for_each_concurrent},
|
||||||
@ -127,6 +128,7 @@ pub async fn download_client(
|
|||||||
&client_download.url,
|
&client_download.url,
|
||||||
Some(&client_download.sha1),
|
Some(&client_download.sha1),
|
||||||
&st.fetch_semaphore,
|
&st.fetch_semaphore,
|
||||||
|
&CredentialsStore(None),
|
||||||
)
|
)
|
||||||
.await?;
|
.await?;
|
||||||
write(&path, &bytes, &st.io_semaphore).await?;
|
write(&path, &bytes, &st.io_semaphore).await?;
|
||||||
@ -206,7 +208,7 @@ pub async fn download_assets(
|
|||||||
async {
|
async {
|
||||||
if !resource_path.exists() {
|
if !resource_path.exists() {
|
||||||
let resource = fetch_cell
|
let resource = fetch_cell
|
||||||
.get_or_try_init(|| fetch(&url, Some(hash), &st.fetch_semaphore))
|
.get_or_try_init(|| fetch(&url, Some(hash), &st.fetch_semaphore, &CredentialsStore(None)))
|
||||||
.await?;
|
.await?;
|
||||||
write(&resource_path, resource, &st.io_semaphore).await?;
|
write(&resource_path, resource, &st.io_semaphore).await?;
|
||||||
tracing::trace!("Fetched asset with hash {hash}");
|
tracing::trace!("Fetched asset with hash {hash}");
|
||||||
@ -216,7 +218,7 @@ pub async fn download_assets(
|
|||||||
async {
|
async {
|
||||||
if with_legacy {
|
if with_legacy {
|
||||||
let resource = fetch_cell
|
let resource = fetch_cell
|
||||||
.get_or_try_init(|| fetch(&url, Some(hash), &st.fetch_semaphore))
|
.get_or_try_init(|| fetch(&url, Some(hash), &st.fetch_semaphore, &CredentialsStore(None)))
|
||||||
.await?;
|
.await?;
|
||||||
let resource_path = st.directories.legacy_assets_dir().await.join(
|
let resource_path = st.directories.legacy_assets_dir().await.join(
|
||||||
name.replace('/', &String::from(std::path::MAIN_SEPARATOR))
|
name.replace('/', &String::from(std::path::MAIN_SEPARATOR))
|
||||||
@ -273,7 +275,7 @@ pub async fn download_libraries(
|
|||||||
artifact: Some(ref artifact),
|
artifact: Some(ref artifact),
|
||||||
..
|
..
|
||||||
}) => {
|
}) => {
|
||||||
let bytes = fetch(&artifact.url, Some(&artifact.sha1), &st.fetch_semaphore)
|
let bytes = fetch(&artifact.url, Some(&artifact.sha1), &st.fetch_semaphore, &CredentialsStore(None))
|
||||||
.await?;
|
.await?;
|
||||||
write(&path, &bytes, &st.io_semaphore).await?;
|
write(&path, &bytes, &st.io_semaphore).await?;
|
||||||
tracing::trace!("Fetched library {} to path {:?}", &library.name, &path);
|
tracing::trace!("Fetched library {} to path {:?}", &library.name, &path);
|
||||||
@ -288,7 +290,7 @@ pub async fn download_libraries(
|
|||||||
&artifact_path
|
&artifact_path
|
||||||
].concat();
|
].concat();
|
||||||
|
|
||||||
let bytes = fetch(&url, None, &st.fetch_semaphore).await?;
|
let bytes = fetch(&url, None, &st.fetch_semaphore, &CredentialsStore(None)).await?;
|
||||||
write(&path, &bytes, &st.io_semaphore).await?;
|
write(&path, &bytes, &st.io_semaphore).await?;
|
||||||
tracing::trace!("Fetched library {} to path {:?}", &library.name, &path);
|
tracing::trace!("Fetched library {} to path {:?}", &library.name, &path);
|
||||||
Ok::<_, crate::Error>(())
|
Ok::<_, crate::Error>(())
|
||||||
@ -314,7 +316,7 @@ pub async fn download_libraries(
|
|||||||
);
|
);
|
||||||
|
|
||||||
if let Some(native) = classifiers.get(&parsed_key) {
|
if let Some(native) = classifiers.get(&parsed_key) {
|
||||||
let data = fetch(&native.url, Some(&native.sha1), &st.fetch_semaphore).await?;
|
let data = fetch(&native.url, Some(&native.sha1), &st.fetch_semaphore, &CredentialsStore(None)).await?;
|
||||||
let reader = std::io::Cursor::new(&data);
|
let reader = std::io::Cursor::new(&data);
|
||||||
if let Ok(mut archive) = zip::ZipArchive::new(reader) {
|
if let Ok(mut archive) = zip::ZipArchive::new(reader) {
|
||||||
match archive.extract(st.directories.version_natives_dir(version).await) {
|
match archive.extract(st.directories.version_natives_dir(version).await) {
|
||||||
|
|||||||
@ -6,7 +6,10 @@ use tokio::task::JoinHandle;
|
|||||||
// A wrapper over the authentication task that allows it to be called from the frontend
|
// A wrapper over the authentication task that allows it to be called from the frontend
|
||||||
// without caching the task handle in the frontend
|
// without caching the task handle in the frontend
|
||||||
|
|
||||||
pub struct AuthTask(Option<JoinHandle<crate::Result<Credentials>>>);
|
pub struct AuthTask(
|
||||||
|
#[allow(clippy::type_complexity)]
|
||||||
|
Option<JoinHandle<crate::Result<(Credentials, Option<String>)>>>,
|
||||||
|
);
|
||||||
|
|
||||||
impl AuthTask {
|
impl AuthTask {
|
||||||
pub fn new() -> AuthTask {
|
pub fn new() -> AuthTask {
|
||||||
@ -37,7 +40,8 @@ impl AuthTask {
|
|||||||
Ok(url)
|
Ok(url)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn await_auth_completion() -> crate::Result<Credentials> {
|
pub async fn await_auth_completion(
|
||||||
|
) -> crate::Result<(Credentials, Option<String>)> {
|
||||||
// Gets the task handle from the state, replacing with None
|
// Gets the task handle from the state, replacing with None
|
||||||
let task = {
|
let task = {
|
||||||
let state = crate::State::get().await?;
|
let state = crate::State::get().await?;
|
||||||
|
|||||||
@ -52,6 +52,9 @@ pub use self::safe_processes::*;
|
|||||||
mod discord;
|
mod discord;
|
||||||
pub use self::discord::*;
|
pub use self::discord::*;
|
||||||
|
|
||||||
|
mod mr_auth;
|
||||||
|
pub use self::mr_auth::*;
|
||||||
|
|
||||||
// Global state
|
// Global state
|
||||||
// RwLock on state only has concurrent reads, except for config dir change which takes control of the State
|
// RwLock on state only has concurrent reads, except for config dir change which takes control of the State
|
||||||
static LAUNCHER_STATE: OnceCell<RwLock<State>> = OnceCell::const_new();
|
static LAUNCHER_STATE: OnceCell<RwLock<State>> = OnceCell::const_new();
|
||||||
@ -77,16 +80,20 @@ pub struct State {
|
|||||||
pub settings: RwLock<Settings>,
|
pub settings: RwLock<Settings>,
|
||||||
/// Reference to minecraft process children
|
/// Reference to minecraft process children
|
||||||
pub children: RwLock<Children>,
|
pub children: RwLock<Children>,
|
||||||
/// Authentication flow
|
|
||||||
pub auth_flow: RwLock<AuthTask>,
|
|
||||||
/// Launcher profile metadata
|
/// Launcher profile metadata
|
||||||
pub(crate) profiles: RwLock<Profiles>,
|
pub(crate) profiles: RwLock<Profiles>,
|
||||||
/// Launcher user account info
|
|
||||||
pub(crate) users: RwLock<Users>,
|
|
||||||
/// Launcher tags
|
/// Launcher tags
|
||||||
pub(crate) tags: RwLock<Tags>,
|
pub(crate) tags: RwLock<Tags>,
|
||||||
/// Launcher processes that should be safely exited on shutdown
|
/// Launcher processes that should be safely exited on shutdown
|
||||||
pub(crate) safety_processes: RwLock<SafeProcesses>,
|
pub(crate) safety_processes: RwLock<SafeProcesses>,
|
||||||
|
/// Launcher user account info
|
||||||
|
pub(crate) users: RwLock<Users>,
|
||||||
|
/// Authentication flow
|
||||||
|
pub auth_flow: RwLock<AuthTask>,
|
||||||
|
/// Modrinth Credentials Store
|
||||||
|
pub credentials: RwLock<CredentialsStore>,
|
||||||
|
/// Modrinth auth flow
|
||||||
|
pub modrinth_auth_flow: RwLock<Option<ModrinthAuthFlow>>,
|
||||||
|
|
||||||
/// Discord RPC
|
/// Discord RPC
|
||||||
pub discord_rpc: DiscordGuard,
|
pub discord_rpc: DiscordGuard,
|
||||||
@ -159,15 +166,18 @@ impl State {
|
|||||||
!is_offline,
|
!is_offline,
|
||||||
&io_semaphore,
|
&io_semaphore,
|
||||||
&fetch_semaphore,
|
&fetch_semaphore,
|
||||||
|
&CredentialsStore(None),
|
||||||
);
|
);
|
||||||
let users_fut = Users::init(&directories, &io_semaphore);
|
let users_fut = Users::init(&directories, &io_semaphore);
|
||||||
|
let creds_fut = CredentialsStore::init(&directories, &io_semaphore);
|
||||||
// Launcher data
|
// Launcher data
|
||||||
let (metadata, profiles, tags, users) = loading_join! {
|
let (metadata, profiles, tags, users, creds) = loading_join! {
|
||||||
Some(&loading_bar), 70.0, Some("Loading metadata");
|
Some(&loading_bar), 70.0, Some("Loading metadata");
|
||||||
metadata_fut,
|
metadata_fut,
|
||||||
profiles_fut,
|
profiles_fut,
|
||||||
tags_fut,
|
tags_fut,
|
||||||
users_fut,
|
users_fut,
|
||||||
|
creds_fut,
|
||||||
}?;
|
}?;
|
||||||
|
|
||||||
let children = Children::new();
|
let children = Children::new();
|
||||||
@ -198,10 +208,12 @@ impl State {
|
|||||||
users: RwLock::new(users),
|
users: RwLock::new(users),
|
||||||
children: RwLock::new(children),
|
children: RwLock::new(children),
|
||||||
auth_flow: RwLock::new(auth_flow),
|
auth_flow: RwLock::new(auth_flow),
|
||||||
|
credentials: RwLock::new(creds),
|
||||||
tags: RwLock::new(tags),
|
tags: RwLock::new(tags),
|
||||||
discord_rpc,
|
discord_rpc,
|
||||||
safety_processes: RwLock::new(safety_processes),
|
safety_processes: RwLock::new(safety_processes),
|
||||||
file_watcher: RwLock::new(file_watcher),
|
file_watcher: RwLock::new(file_watcher),
|
||||||
|
modrinth_auth_flow: RwLock::new(None),
|
||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -222,6 +234,11 @@ impl State {
|
|||||||
|
|
||||||
/// Updates state with data from the web, if we are online
|
/// Updates state with data from the web, if we are online
|
||||||
pub fn update() {
|
pub fn update() {
|
||||||
|
tokio::task::spawn(Metadata::update());
|
||||||
|
tokio::task::spawn(Tags::update());
|
||||||
|
tokio::task::spawn(Profiles::update_projects());
|
||||||
|
tokio::task::spawn(Profiles::update_modrinth_versions());
|
||||||
|
tokio::task::spawn(CredentialsStore::update_creds());
|
||||||
tokio::task::spawn(async {
|
tokio::task::spawn(async {
|
||||||
if let Ok(state) = crate::State::get().await {
|
if let Ok(state) = crate::State::get().await {
|
||||||
if !*state.offline.read().await {
|
if !*state.offline.read().await {
|
||||||
@ -230,8 +247,9 @@ impl State {
|
|||||||
let res3 = Metadata::update();
|
let res3 = Metadata::update();
|
||||||
let res4 = Profiles::update_projects();
|
let res4 = Profiles::update_projects();
|
||||||
let res5 = Settings::update_java();
|
let res5 = Settings::update_java();
|
||||||
|
let res6 = CredentialsStore::update_creds();
|
||||||
|
|
||||||
let _ = join!(res1, res2, res3, res4, res5);
|
let _ = join!(res1, res2, res3, res4, res5, res6);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|||||||
398
theseus/src/state/mr_auth.rs
Normal file
398
theseus/src/state/mr_auth.rs
Normal file
@ -0,0 +1,398 @@
|
|||||||
|
use crate::config::MODRINTH_API_URL;
|
||||||
|
use crate::state::DirectoryInfo;
|
||||||
|
use crate::util::fetch::{
|
||||||
|
fetch_advanced, read_json, write, FetchSemaphore, IoSemaphore,
|
||||||
|
};
|
||||||
|
use crate::State;
|
||||||
|
use chrono::{DateTime, Duration, Utc};
|
||||||
|
use futures::TryStreamExt;
|
||||||
|
use reqwest::Method;
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
use serde_json::Value;
|
||||||
|
use std::collections::HashMap;
|
||||||
|
|
||||||
|
const AUTH_JSON: &str = "auth.json";
|
||||||
|
|
||||||
|
#[derive(Serialize, Deserialize, Clone, Debug)]
|
||||||
|
pub struct ModrinthUser {
|
||||||
|
pub id: String,
|
||||||
|
pub username: String,
|
||||||
|
pub name: Option<String>,
|
||||||
|
pub avatar_url: Option<String>,
|
||||||
|
pub bio: Option<String>,
|
||||||
|
pub created: DateTime<Utc>,
|
||||||
|
pub role: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize, Deserialize, Clone, Debug)]
|
||||||
|
pub struct ModrinthCredentials {
|
||||||
|
pub session: String,
|
||||||
|
pub expires_at: DateTime<Utc>,
|
||||||
|
pub user: ModrinthUser,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize)]
|
||||||
|
#[serde(tag = "type")]
|
||||||
|
#[serde(rename_all = "snake_case")]
|
||||||
|
pub enum ModrinthCredentialsResult {
|
||||||
|
TwoFactorRequired { flow: String },
|
||||||
|
Credentials(ModrinthCredentials),
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub struct CredentialsStore(pub Option<ModrinthCredentials>);
|
||||||
|
|
||||||
|
impl CredentialsStore {
|
||||||
|
pub async fn init(
|
||||||
|
dirs: &DirectoryInfo,
|
||||||
|
io_semaphore: &IoSemaphore,
|
||||||
|
) -> crate::Result<Self> {
|
||||||
|
let auth_path = dirs.caches_meta_dir().await.join(AUTH_JSON);
|
||||||
|
let user = read_json(&auth_path, io_semaphore).await.ok();
|
||||||
|
|
||||||
|
if let Some(user) = user {
|
||||||
|
Ok(Self(Some(user)))
|
||||||
|
} else {
|
||||||
|
Ok(Self(None))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn save(&self) -> crate::Result<()> {
|
||||||
|
let state = State::get().await?;
|
||||||
|
let auth_path =
|
||||||
|
state.directories.caches_meta_dir().await.join(AUTH_JSON);
|
||||||
|
|
||||||
|
if let Some(creds) = &self.0 {
|
||||||
|
write(&auth_path, &serde_json::to_vec(creds)?, &state.io_semaphore)
|
||||||
|
.await?;
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn login(
|
||||||
|
&mut self,
|
||||||
|
credentials: ModrinthCredentials,
|
||||||
|
) -> crate::Result<&Self> {
|
||||||
|
self.0 = Some(credentials);
|
||||||
|
self.save().await?;
|
||||||
|
Ok(self)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tracing::instrument]
|
||||||
|
pub async fn update_creds() {
|
||||||
|
let res = async {
|
||||||
|
let state = State::get().await?;
|
||||||
|
let mut creds_write = state.credentials.write().await;
|
||||||
|
|
||||||
|
refresh_credentials(&mut creds_write, &state.fetch_semaphore)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
Ok::<(), crate::Error>(())
|
||||||
|
}
|
||||||
|
.await;
|
||||||
|
|
||||||
|
match res {
|
||||||
|
Ok(()) => {}
|
||||||
|
Err(err) => {
|
||||||
|
tracing::warn!("Unable to update credentials: {err}")
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn logout(&mut self) -> crate::Result<&Self> {
|
||||||
|
self.0 = None;
|
||||||
|
self.save().await?;
|
||||||
|
Ok(self)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct ModrinthAuthFlow {
|
||||||
|
socket: async_tungstenite::WebSocketStream<
|
||||||
|
async_tungstenite::tokio::ConnectStream,
|
||||||
|
>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ModrinthAuthFlow {
|
||||||
|
pub async fn new(provider: &str) -> crate::Result<Self> {
|
||||||
|
let (socket, _) = async_tungstenite::tokio::connect_async(format!(
|
||||||
|
"wss://api.modrinth.com/v2/auth/ws?provider={provider}"
|
||||||
|
))
|
||||||
|
.await?;
|
||||||
|
Ok(Self { socket })
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn prepare_login_url(&mut self) -> crate::Result<String> {
|
||||||
|
let code_resp = self
|
||||||
|
.socket
|
||||||
|
.try_next()
|
||||||
|
.await?
|
||||||
|
.ok_or(
|
||||||
|
crate::ErrorKind::WSClosedError(String::from(
|
||||||
|
"login socket URL",
|
||||||
|
))
|
||||||
|
.as_error(),
|
||||||
|
)?
|
||||||
|
.into_data();
|
||||||
|
|
||||||
|
#[derive(Deserialize)]
|
||||||
|
struct Url {
|
||||||
|
url: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
let response = serde_json::from_slice::<Url>(&code_resp)?;
|
||||||
|
|
||||||
|
Ok(response.url)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn extract_credentials(
|
||||||
|
&mut self,
|
||||||
|
semaphore: &FetchSemaphore,
|
||||||
|
) -> crate::Result<ModrinthCredentialsResult> {
|
||||||
|
// Minecraft bearer token
|
||||||
|
let token_resp = self
|
||||||
|
.socket
|
||||||
|
.try_next()
|
||||||
|
.await?
|
||||||
|
.ok_or(
|
||||||
|
crate::ErrorKind::WSClosedError(String::from(
|
||||||
|
"login socket URL",
|
||||||
|
))
|
||||||
|
.as_error(),
|
||||||
|
)?
|
||||||
|
.into_data();
|
||||||
|
|
||||||
|
let response =
|
||||||
|
serde_json::from_slice::<HashMap<String, Value>>(&token_resp)?;
|
||||||
|
|
||||||
|
get_result_from_res("code", response, semaphore).await
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn close(&mut self) -> crate::Result<()> {
|
||||||
|
self.socket.close(None).await?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn get_result_from_res(
|
||||||
|
code_key: &str,
|
||||||
|
response: HashMap<String, Value>,
|
||||||
|
semaphore: &FetchSemaphore,
|
||||||
|
) -> crate::Result<ModrinthCredentialsResult> {
|
||||||
|
if let Some(flow) = response.get("flow").and_then(|x| x.as_str()) {
|
||||||
|
Ok(ModrinthCredentialsResult::TwoFactorRequired {
|
||||||
|
flow: flow.to_string(),
|
||||||
|
})
|
||||||
|
} else if let Some(code) = response.get(code_key).and_then(|x| x.as_str()) {
|
||||||
|
let info = fetch_info(code, semaphore).await?;
|
||||||
|
|
||||||
|
Ok(ModrinthCredentialsResult::Credentials(
|
||||||
|
ModrinthCredentials {
|
||||||
|
session: code.to_string(),
|
||||||
|
expires_at: Utc::now() + Duration::weeks(2),
|
||||||
|
user: info,
|
||||||
|
},
|
||||||
|
))
|
||||||
|
} else if let Some(error) =
|
||||||
|
response.get("description").and_then(|x| x.as_str())
|
||||||
|
{
|
||||||
|
Err(crate::ErrorKind::OtherError(format!(
|
||||||
|
"Failed to login with error {error}"
|
||||||
|
))
|
||||||
|
.as_error())
|
||||||
|
} else {
|
||||||
|
Err(crate::ErrorKind::OtherError(String::from(
|
||||||
|
"Flow/code/error not found in response!",
|
||||||
|
))
|
||||||
|
.as_error())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Deserialize)]
|
||||||
|
struct Session {
|
||||||
|
session: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn login_password(
|
||||||
|
username: &str,
|
||||||
|
password: &str,
|
||||||
|
challenge: &str,
|
||||||
|
semaphore: &FetchSemaphore,
|
||||||
|
) -> crate::Result<ModrinthCredentialsResult> {
|
||||||
|
let resp = fetch_advanced(
|
||||||
|
Method::POST,
|
||||||
|
&format!("https://{MODRINTH_API_URL}auth/login"),
|
||||||
|
None,
|
||||||
|
Some(serde_json::json!({
|
||||||
|
"username": username,
|
||||||
|
"password": password,
|
||||||
|
"challenge": challenge,
|
||||||
|
})),
|
||||||
|
None,
|
||||||
|
None,
|
||||||
|
semaphore,
|
||||||
|
&CredentialsStore(None),
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
let value = serde_json::from_slice::<HashMap<String, Value>>(&resp)?;
|
||||||
|
|
||||||
|
get_result_from_res("session", value, semaphore).await
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn get_creds_from_res(
|
||||||
|
response: HashMap<String, Value>,
|
||||||
|
semaphore: &FetchSemaphore,
|
||||||
|
) -> crate::Result<ModrinthCredentials> {
|
||||||
|
if let Some(code) = response.get("session").and_then(|x| x.as_str()) {
|
||||||
|
let info = fetch_info(code, semaphore).await?;
|
||||||
|
|
||||||
|
Ok(ModrinthCredentials {
|
||||||
|
session: code.to_string(),
|
||||||
|
expires_at: Utc::now() + Duration::weeks(2),
|
||||||
|
user: info,
|
||||||
|
})
|
||||||
|
} else if let Some(error) =
|
||||||
|
response.get("description").and_then(|x| x.as_str())
|
||||||
|
{
|
||||||
|
Err(crate::ErrorKind::OtherError(format!(
|
||||||
|
"Failed to login with error {error}"
|
||||||
|
))
|
||||||
|
.as_error())
|
||||||
|
} else {
|
||||||
|
Err(crate::ErrorKind::OtherError(String::from(
|
||||||
|
"Flow/code/error not found in response!",
|
||||||
|
))
|
||||||
|
.as_error())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn login_2fa(
|
||||||
|
code: &str,
|
||||||
|
flow: &str,
|
||||||
|
semaphore: &FetchSemaphore,
|
||||||
|
) -> crate::Result<ModrinthCredentials> {
|
||||||
|
let resp = fetch_advanced(
|
||||||
|
Method::POST,
|
||||||
|
&format!("{MODRINTH_API_URL}auth/login/2fa"),
|
||||||
|
None,
|
||||||
|
Some(serde_json::json!({
|
||||||
|
"code": code,
|
||||||
|
"flow": flow,
|
||||||
|
})),
|
||||||
|
None,
|
||||||
|
None,
|
||||||
|
semaphore,
|
||||||
|
&CredentialsStore(None),
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
let response = serde_json::from_slice::<HashMap<String, Value>>(&resp)?;
|
||||||
|
|
||||||
|
get_creds_from_res(response, semaphore).await
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn create_account(
|
||||||
|
username: &str,
|
||||||
|
email: &str,
|
||||||
|
password: &str,
|
||||||
|
challenge: &str,
|
||||||
|
sign_up_newsletter: bool,
|
||||||
|
semaphore: &FetchSemaphore,
|
||||||
|
) -> crate::Result<ModrinthCredentials> {
|
||||||
|
let resp = fetch_advanced(
|
||||||
|
Method::POST,
|
||||||
|
&format!("{MODRINTH_API_URL}auth/create"),
|
||||||
|
None,
|
||||||
|
Some(serde_json::json!({
|
||||||
|
"username": username,
|
||||||
|
"email": email,
|
||||||
|
"password": password,
|
||||||
|
"challenge": challenge,
|
||||||
|
"sign_up_newsletter": sign_up_newsletter,
|
||||||
|
})),
|
||||||
|
None,
|
||||||
|
None,
|
||||||
|
semaphore,
|
||||||
|
&CredentialsStore(None),
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
let response = serde_json::from_slice::<HashMap<String, Value>>(&resp)?;
|
||||||
|
|
||||||
|
get_creds_from_res(response, semaphore).await
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn login_minecraft(
|
||||||
|
flow: &str,
|
||||||
|
semaphore: &FetchSemaphore,
|
||||||
|
) -> crate::Result<ModrinthCredentialsResult> {
|
||||||
|
let resp = fetch_advanced(
|
||||||
|
Method::POST,
|
||||||
|
&format!("{MODRINTH_API_URL}auth/login/minecraft"),
|
||||||
|
None,
|
||||||
|
Some(serde_json::json!({
|
||||||
|
"flow": flow,
|
||||||
|
})),
|
||||||
|
None,
|
||||||
|
None,
|
||||||
|
semaphore,
|
||||||
|
&CredentialsStore(None),
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
let response = serde_json::from_slice::<HashMap<String, Value>>(&resp)?;
|
||||||
|
|
||||||
|
get_result_from_res("session", response, semaphore).await
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn refresh_credentials(
|
||||||
|
credentials_store: &mut CredentialsStore,
|
||||||
|
semaphore: &FetchSemaphore,
|
||||||
|
) -> crate::Result<()> {
|
||||||
|
if let Some(ref mut credentials) = credentials_store.0 {
|
||||||
|
let token = &credentials.session;
|
||||||
|
let resp = fetch_advanced(
|
||||||
|
Method::POST,
|
||||||
|
&format!("{MODRINTH_API_URL}session/refresh"),
|
||||||
|
None,
|
||||||
|
None,
|
||||||
|
Some(("Authorization", token)),
|
||||||
|
None,
|
||||||
|
semaphore,
|
||||||
|
&CredentialsStore(None),
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
.ok()
|
||||||
|
.and_then(|resp| serde_json::from_slice::<Session>(&resp).ok());
|
||||||
|
|
||||||
|
if let Some(value) = resp {
|
||||||
|
credentials.user = fetch_info(&value.session, semaphore).await?;
|
||||||
|
credentials.session = value.session;
|
||||||
|
credentials.expires_at = Utc::now() + Duration::weeks(2);
|
||||||
|
} else if credentials.expires_at < Utc::now() {
|
||||||
|
credentials_store.0 = None;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn fetch_info(
|
||||||
|
token: &str,
|
||||||
|
semaphore: &FetchSemaphore,
|
||||||
|
) -> crate::Result<ModrinthUser> {
|
||||||
|
let result = fetch_advanced(
|
||||||
|
Method::GET,
|
||||||
|
&format!("{MODRINTH_API_URL}user"),
|
||||||
|
None,
|
||||||
|
None,
|
||||||
|
Some(("Authorization", token)),
|
||||||
|
None,
|
||||||
|
semaphore,
|
||||||
|
&CredentialsStore(None),
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
let value = serde_json::from_slice(&result)?;
|
||||||
|
|
||||||
|
Ok(value)
|
||||||
|
}
|
||||||
@ -342,14 +342,17 @@ impl Profile {
|
|||||||
let paths = profile.get_profile_full_project_paths().await?;
|
let paths = profile.get_profile_full_project_paths().await?;
|
||||||
|
|
||||||
let caches_dir = state.directories.caches_dir();
|
let caches_dir = state.directories.caches_dir();
|
||||||
|
let creds = state.credentials.read().await;
|
||||||
let projects = crate::state::infer_data_from_files(
|
let projects = crate::state::infer_data_from_files(
|
||||||
profile.clone(),
|
profile.clone(),
|
||||||
paths,
|
paths,
|
||||||
caches_dir,
|
caches_dir,
|
||||||
&state.io_semaphore,
|
&state.io_semaphore,
|
||||||
&state.fetch_semaphore,
|
&state.fetch_semaphore,
|
||||||
|
&creds,
|
||||||
)
|
)
|
||||||
.await?;
|
.await?;
|
||||||
|
drop(creds);
|
||||||
|
|
||||||
let mut new_profiles = state.profiles.write().await;
|
let mut new_profiles = state.profiles.write().await;
|
||||||
if let Some(profile) = new_profiles.0.get_mut(&profile_path_id) {
|
if let Some(profile) = new_profiles.0.get_mut(&profile_path_id) {
|
||||||
@ -462,14 +465,17 @@ impl Profile {
|
|||||||
version_id: String,
|
version_id: String,
|
||||||
) -> crate::Result<(ProjectPathId, ModrinthVersion)> {
|
) -> crate::Result<(ProjectPathId, ModrinthVersion)> {
|
||||||
let state = State::get().await?;
|
let state = State::get().await?;
|
||||||
|
let creds = state.credentials.read().await;
|
||||||
let version = fetch_json::<ModrinthVersion>(
|
let version = fetch_json::<ModrinthVersion>(
|
||||||
Method::GET,
|
Method::GET,
|
||||||
&format!("{MODRINTH_API_URL}version/{version_id}"),
|
&format!("{MODRINTH_API_URL}version/{version_id}"),
|
||||||
None,
|
None,
|
||||||
None,
|
None,
|
||||||
&state.fetch_semaphore,
|
&state.fetch_semaphore,
|
||||||
|
&creds,
|
||||||
)
|
)
|
||||||
.await?;
|
.await?;
|
||||||
|
drop(creds);
|
||||||
let file = if let Some(file) = version.files.iter().find(|x| x.primary)
|
let file = if let Some(file) = version.files.iter().find(|x| x.primary)
|
||||||
{
|
{
|
||||||
file
|
file
|
||||||
@ -482,12 +488,15 @@ impl Profile {
|
|||||||
.into());
|
.into());
|
||||||
};
|
};
|
||||||
|
|
||||||
|
let creds = state.credentials.read().await;
|
||||||
let bytes = fetch(
|
let bytes = fetch(
|
||||||
&file.url,
|
&file.url,
|
||||||
file.hashes.get("sha1").map(|x| &**x),
|
file.hashes.get("sha1").map(|x| &**x),
|
||||||
&state.fetch_semaphore,
|
&state.fetch_semaphore,
|
||||||
|
&creds,
|
||||||
)
|
)
|
||||||
.await?;
|
.await?;
|
||||||
|
drop(creds);
|
||||||
let path = self
|
let path = self
|
||||||
.add_project_bytes(
|
.add_project_bytes(
|
||||||
&file.filename,
|
&file.filename,
|
||||||
@ -736,14 +745,17 @@ impl Profiles {
|
|||||||
future::try_join_all(files.into_iter().map(
|
future::try_join_all(files.into_iter().map(
|
||||||
|(profile, files)| async {
|
|(profile, files)| async {
|
||||||
let profile_name = profile.profile_id();
|
let profile_name = profile.profile_id();
|
||||||
|
let creds = state.credentials.read().await;
|
||||||
let inferred = super::projects::infer_data_from_files(
|
let inferred = super::projects::infer_data_from_files(
|
||||||
profile,
|
profile,
|
||||||
files,
|
files,
|
||||||
caches_dir.clone(),
|
caches_dir.clone(),
|
||||||
&state.io_semaphore,
|
&state.io_semaphore,
|
||||||
&state.fetch_semaphore,
|
&state.fetch_semaphore,
|
||||||
|
&creds,
|
||||||
)
|
)
|
||||||
.await?;
|
.await?;
|
||||||
|
drop(creds);
|
||||||
|
|
||||||
let mut new_profiles = state.profiles.write().await;
|
let mut new_profiles = state.profiles.write().await;
|
||||||
if let Some(profile) = new_profiles.0.get_mut(&profile_name)
|
if let Some(profile) = new_profiles.0.get_mut(&profile_name)
|
||||||
@ -803,6 +815,7 @@ impl Profiles {
|
|||||||
let linked_project = linked_project;
|
let linked_project = linked_project;
|
||||||
let state = state.clone();
|
let state = state.clone();
|
||||||
async move {
|
async move {
|
||||||
|
let creds = state.credentials.read().await;
|
||||||
let versions: Vec<ModrinthVersion> = fetch_json(
|
let versions: Vec<ModrinthVersion> = fetch_json(
|
||||||
Method::GET,
|
Method::GET,
|
||||||
&format!(
|
&format!(
|
||||||
@ -813,8 +826,10 @@ impl Profiles {
|
|||||||
None,
|
None,
|
||||||
None,
|
None,
|
||||||
&state.fetch_semaphore,
|
&state.fetch_semaphore,
|
||||||
|
&creds,
|
||||||
)
|
)
|
||||||
.await?;
|
.await?;
|
||||||
|
drop(creds);
|
||||||
|
|
||||||
// Versions are pre-sorted in labrinth (by versions.sort_by(|a, b| b.inner.date_published.cmp(&a.inner.date_published));)
|
// Versions are pre-sorted in labrinth (by versions.sort_by(|a, b| b.inner.date_published.cmp(&a.inner.date_published));)
|
||||||
// so we can just take the first one
|
// so we can just take the first one
|
||||||
|
|||||||
@ -1,7 +1,7 @@
|
|||||||
//! Project management + inference
|
//! Project management + inference
|
||||||
|
|
||||||
use crate::config::MODRINTH_API_URL;
|
use crate::config::MODRINTH_API_URL;
|
||||||
use crate::state::Profile;
|
use crate::state::{CredentialsStore, ModrinthUser, Profile};
|
||||||
use crate::util::fetch::{
|
use crate::util::fetch::{
|
||||||
fetch_json, write_cached_icon, FetchSemaphore, IoSemaphore,
|
fetch_json, write_cached_icon, FetchSemaphore, IoSemaphore,
|
||||||
};
|
};
|
||||||
@ -168,18 +168,6 @@ pub struct ModrinthTeamMember {
|
|||||||
pub ordering: i64,
|
pub ordering: i64,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Serialize, Deserialize, Clone, Debug)]
|
|
||||||
pub struct ModrinthUser {
|
|
||||||
pub id: String,
|
|
||||||
pub github_id: Option<u64>,
|
|
||||||
pub username: String,
|
|
||||||
pub name: Option<String>,
|
|
||||||
pub avatar_url: Option<String>,
|
|
||||||
pub bio: Option<String>,
|
|
||||||
pub created: DateTime<Utc>,
|
|
||||||
pub role: String,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Serialize, Deserialize, Copy, Clone, Debug)]
|
#[derive(Serialize, Deserialize, Copy, Clone, Debug)]
|
||||||
#[serde(rename_all = "lowercase")]
|
#[serde(rename_all = "lowercase")]
|
||||||
pub enum DependencyType {
|
pub enum DependencyType {
|
||||||
@ -289,6 +277,7 @@ pub async fn infer_data_from_files(
|
|||||||
cache_dir: PathBuf,
|
cache_dir: PathBuf,
|
||||||
io_semaphore: &IoSemaphore,
|
io_semaphore: &IoSemaphore,
|
||||||
fetch_semaphore: &FetchSemaphore,
|
fetch_semaphore: &FetchSemaphore,
|
||||||
|
credentials: &CredentialsStore,
|
||||||
) -> crate::Result<HashMap<ProjectPathId, Project>> {
|
) -> crate::Result<HashMap<ProjectPathId, Project>> {
|
||||||
let mut file_path_hashes = HashMap::new();
|
let mut file_path_hashes = HashMap::new();
|
||||||
|
|
||||||
@ -327,6 +316,7 @@ pub async fn infer_data_from_files(
|
|||||||
"algorithm": "sha512",
|
"algorithm": "sha512",
|
||||||
})),
|
})),
|
||||||
fetch_semaphore,
|
fetch_semaphore,
|
||||||
|
credentials,
|
||||||
),
|
),
|
||||||
fetch_json::<HashMap<String, ModrinthVersion>>(
|
fetch_json::<HashMap<String, ModrinthVersion>>(
|
||||||
Method::POST,
|
Method::POST,
|
||||||
@ -339,6 +329,7 @@ pub async fn infer_data_from_files(
|
|||||||
"game_versions": [profile.metadata.game_version]
|
"game_versions": [profile.metadata.game_version]
|
||||||
})),
|
})),
|
||||||
fetch_semaphore,
|
fetch_semaphore,
|
||||||
|
credentials,
|
||||||
)
|
)
|
||||||
)?;
|
)?;
|
||||||
|
|
||||||
@ -357,6 +348,7 @@ pub async fn infer_data_from_files(
|
|||||||
None,
|
None,
|
||||||
None,
|
None,
|
||||||
fetch_semaphore,
|
fetch_semaphore,
|
||||||
|
credentials,
|
||||||
)
|
)
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
@ -374,6 +366,7 @@ pub async fn infer_data_from_files(
|
|||||||
None,
|
None,
|
||||||
None,
|
None,
|
||||||
fetch_semaphore,
|
fetch_semaphore,
|
||||||
|
credentials,
|
||||||
)
|
)
|
||||||
.await?
|
.await?
|
||||||
.into_iter()
|
.into_iter()
|
||||||
|
|||||||
@ -41,7 +41,7 @@ pub struct Settings {
|
|||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
pub advanced_rendering: bool,
|
pub advanced_rendering: bool,
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
pub onboarded_new: bool,
|
pub fully_onboarded: bool,
|
||||||
#[serde(default = "DirectoryInfo::get_initial_settings_dir")]
|
#[serde(default = "DirectoryInfo::get_initial_settings_dir")]
|
||||||
pub loaded_config_dir: Option<PathBuf>,
|
pub loaded_config_dir: Option<PathBuf>,
|
||||||
}
|
}
|
||||||
@ -82,7 +82,7 @@ impl Settings {
|
|||||||
developer_mode: false,
|
developer_mode: false,
|
||||||
opt_out_analytics: false,
|
opt_out_analytics: false,
|
||||||
advanced_rendering: true,
|
advanced_rendering: true,
|
||||||
onboarded_new: false,
|
fully_onboarded: false,
|
||||||
|
|
||||||
// By default, the config directory is the same as the settings directory
|
// By default, the config directory is the same as the settings directory
|
||||||
loaded_config_dir: DirectoryInfo::get_initial_settings_dir(),
|
loaded_config_dir: DirectoryInfo::get_initial_settings_dir(),
|
||||||
|
|||||||
@ -5,6 +5,7 @@ use serde::{Deserialize, Serialize};
|
|||||||
|
|
||||||
use crate::config::MODRINTH_API_URL;
|
use crate::config::MODRINTH_API_URL;
|
||||||
use crate::data::DirectoryInfo;
|
use crate::data::DirectoryInfo;
|
||||||
|
use crate::state::CredentialsStore;
|
||||||
use crate::util::fetch::{
|
use crate::util::fetch::{
|
||||||
fetch_json, read_json, write, FetchSemaphore, IoSemaphore,
|
fetch_json, read_json, write, FetchSemaphore, IoSemaphore,
|
||||||
};
|
};
|
||||||
@ -27,6 +28,7 @@ impl Tags {
|
|||||||
fetch_online: bool,
|
fetch_online: bool,
|
||||||
io_semaphore: &IoSemaphore,
|
io_semaphore: &IoSemaphore,
|
||||||
fetch_semaphore: &FetchSemaphore,
|
fetch_semaphore: &FetchSemaphore,
|
||||||
|
credentials: &CredentialsStore,
|
||||||
) -> crate::Result<Self> {
|
) -> crate::Result<Self> {
|
||||||
let mut tags = None;
|
let mut tags = None;
|
||||||
let tags_path = dirs.caches_meta_dir().await.join("tags.json");
|
let tags_path = dirs.caches_meta_dir().await.join("tags.json");
|
||||||
@ -35,7 +37,7 @@ impl Tags {
|
|||||||
{
|
{
|
||||||
tags = Some(tags_json);
|
tags = Some(tags_json);
|
||||||
} else if fetch_online {
|
} else if fetch_online {
|
||||||
match Self::fetch(fetch_semaphore).await {
|
match Self::fetch(fetch_semaphore, credentials).await {
|
||||||
Ok(tags_fetch) => tags = Some(tags_fetch),
|
Ok(tags_fetch) => tags = Some(tags_fetch),
|
||||||
Err(err) => {
|
Err(err) => {
|
||||||
tracing::warn!("Unable to fetch launcher tags: {err}")
|
tracing::warn!("Unable to fetch launcher tags: {err}")
|
||||||
@ -58,7 +60,11 @@ impl Tags {
|
|||||||
pub async fn update() {
|
pub async fn update() {
|
||||||
let res = async {
|
let res = async {
|
||||||
let state = crate::State::get().await?;
|
let state = crate::State::get().await?;
|
||||||
let tags_fetch = Tags::fetch(&state.fetch_semaphore).await?;
|
|
||||||
|
let creds = state.credentials.read().await;
|
||||||
|
let tags_fetch =
|
||||||
|
Tags::fetch(&state.fetch_semaphore, &creds).await?;
|
||||||
|
drop(creds);
|
||||||
|
|
||||||
let tags_path =
|
let tags_path =
|
||||||
state.directories.caches_meta_dir().await.join("tags.json");
|
state.directories.caches_meta_dir().await.join("tags.json");
|
||||||
@ -123,7 +129,10 @@ impl Tags {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Fetches the tags from the Modrinth API and stores them in the database
|
// Fetches the tags from the Modrinth API and stores them in the database
|
||||||
pub async fn fetch(semaphore: &FetchSemaphore) -> crate::Result<Self> {
|
pub async fn fetch(
|
||||||
|
semaphore: &FetchSemaphore,
|
||||||
|
credentials: &CredentialsStore,
|
||||||
|
) -> crate::Result<Self> {
|
||||||
let categories = format!("{MODRINTH_API_URL}tag/category");
|
let categories = format!("{MODRINTH_API_URL}tag/category");
|
||||||
let loaders = format!("{MODRINTH_API_URL}tag/loader");
|
let loaders = format!("{MODRINTH_API_URL}tag/loader");
|
||||||
let game_versions = format!("{MODRINTH_API_URL}tag/game_version");
|
let game_versions = format!("{MODRINTH_API_URL}tag/game_version");
|
||||||
@ -137,6 +146,7 @@ impl Tags {
|
|||||||
None,
|
None,
|
||||||
None,
|
None,
|
||||||
semaphore,
|
semaphore,
|
||||||
|
credentials,
|
||||||
);
|
);
|
||||||
let loaders_fut = fetch_json::<Vec<Loader>>(
|
let loaders_fut = fetch_json::<Vec<Loader>>(
|
||||||
Method::GET,
|
Method::GET,
|
||||||
@ -144,6 +154,7 @@ impl Tags {
|
|||||||
None,
|
None,
|
||||||
None,
|
None,
|
||||||
semaphore,
|
semaphore,
|
||||||
|
credentials,
|
||||||
);
|
);
|
||||||
let game_versions_fut = fetch_json::<Vec<GameVersion>>(
|
let game_versions_fut = fetch_json::<Vec<GameVersion>>(
|
||||||
Method::GET,
|
Method::GET,
|
||||||
@ -151,6 +162,7 @@ impl Tags {
|
|||||||
None,
|
None,
|
||||||
None,
|
None,
|
||||||
semaphore,
|
semaphore,
|
||||||
|
credentials,
|
||||||
);
|
);
|
||||||
let donation_platforms_fut = fetch_json::<Vec<DonationPlatform>>(
|
let donation_platforms_fut = fetch_json::<Vec<DonationPlatform>>(
|
||||||
Method::GET,
|
Method::GET,
|
||||||
@ -158,6 +170,7 @@ impl Tags {
|
|||||||
None,
|
None,
|
||||||
None,
|
None,
|
||||||
semaphore,
|
semaphore,
|
||||||
|
credentials,
|
||||||
);
|
);
|
||||||
let report_types_fut = fetch_json::<Vec<String>>(
|
let report_types_fut = fetch_json::<Vec<String>>(
|
||||||
Method::GET,
|
Method::GET,
|
||||||
@ -165,6 +178,7 @@ impl Tags {
|
|||||||
None,
|
None,
|
||||||
None,
|
None,
|
||||||
semaphore,
|
semaphore,
|
||||||
|
credentials,
|
||||||
);
|
);
|
||||||
|
|
||||||
let (
|
let (
|
||||||
|
|||||||
@ -1,6 +1,7 @@
|
|||||||
//! Functions for fetching infromation from the Internet
|
//! Functions for fetching infromation from the Internet
|
||||||
use crate::event::emit::emit_loading;
|
use crate::event::emit::emit_loading;
|
||||||
use crate::event::LoadingBarId;
|
use crate::event::LoadingBarId;
|
||||||
|
use crate::state::CredentialsStore;
|
||||||
use bytes::Bytes;
|
use bytes::Bytes;
|
||||||
use lazy_static::lazy_static;
|
use lazy_static::lazy_static;
|
||||||
use reqwest::Method;
|
use reqwest::Method;
|
||||||
@ -41,8 +42,19 @@ pub async fn fetch(
|
|||||||
url: &str,
|
url: &str,
|
||||||
sha1: Option<&str>,
|
sha1: Option<&str>,
|
||||||
semaphore: &FetchSemaphore,
|
semaphore: &FetchSemaphore,
|
||||||
|
credentials: &CredentialsStore,
|
||||||
) -> crate::Result<Bytes> {
|
) -> crate::Result<Bytes> {
|
||||||
fetch_advanced(Method::GET, url, sha1, None, None, None, semaphore).await
|
fetch_advanced(
|
||||||
|
Method::GET,
|
||||||
|
url,
|
||||||
|
sha1,
|
||||||
|
None,
|
||||||
|
None,
|
||||||
|
None,
|
||||||
|
semaphore,
|
||||||
|
credentials,
|
||||||
|
)
|
||||||
|
.await
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tracing::instrument(skip(json_body, semaphore))]
|
#[tracing::instrument(skip(json_body, semaphore))]
|
||||||
@ -52,13 +64,22 @@ pub async fn fetch_json<T>(
|
|||||||
sha1: Option<&str>,
|
sha1: Option<&str>,
|
||||||
json_body: Option<serde_json::Value>,
|
json_body: Option<serde_json::Value>,
|
||||||
semaphore: &FetchSemaphore,
|
semaphore: &FetchSemaphore,
|
||||||
|
credentials: &CredentialsStore,
|
||||||
) -> crate::Result<T>
|
) -> crate::Result<T>
|
||||||
where
|
where
|
||||||
T: DeserializeOwned,
|
T: DeserializeOwned,
|
||||||
{
|
{
|
||||||
let result =
|
let result = fetch_advanced(
|
||||||
fetch_advanced(method, url, sha1, json_body, None, None, semaphore)
|
method,
|
||||||
.await?;
|
url,
|
||||||
|
sha1,
|
||||||
|
json_body,
|
||||||
|
None,
|
||||||
|
None,
|
||||||
|
semaphore,
|
||||||
|
credentials,
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
let value = serde_json::from_slice(&result)?;
|
let value = serde_json::from_slice(&result)?;
|
||||||
Ok(value)
|
Ok(value)
|
||||||
}
|
}
|
||||||
@ -66,6 +87,7 @@ where
|
|||||||
/// Downloads a file with retry and checksum functionality
|
/// Downloads a file with retry and checksum functionality
|
||||||
#[tracing::instrument(skip(json_body, semaphore))]
|
#[tracing::instrument(skip(json_body, semaphore))]
|
||||||
#[theseus_macros::debug_pin]
|
#[theseus_macros::debug_pin]
|
||||||
|
#[allow(clippy::too_many_arguments)]
|
||||||
pub async fn fetch_advanced(
|
pub async fn fetch_advanced(
|
||||||
method: Method,
|
method: Method,
|
||||||
url: &str,
|
url: &str,
|
||||||
@ -74,6 +96,7 @@ pub async fn fetch_advanced(
|
|||||||
header: Option<(&str, &str)>,
|
header: Option<(&str, &str)>,
|
||||||
loading_bar: Option<(&LoadingBarId, f64)>,
|
loading_bar: Option<(&LoadingBarId, f64)>,
|
||||||
semaphore: &FetchSemaphore,
|
semaphore: &FetchSemaphore,
|
||||||
|
credentials: &CredentialsStore,
|
||||||
) -> crate::Result<Bytes> {
|
) -> crate::Result<Bytes> {
|
||||||
let io_semaphore = semaphore.0.read().await;
|
let io_semaphore = semaphore.0.read().await;
|
||||||
let _permit = io_semaphore.acquire().await?;
|
let _permit = io_semaphore.acquire().await?;
|
||||||
@ -89,6 +112,12 @@ pub async fn fetch_advanced(
|
|||||||
req = req.header(header.0, header.1);
|
req = req.header(header.0, header.1);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if url.starts_with("https://cdn.modrinth.com") {
|
||||||
|
if let Some(creds) = &credentials.0 {
|
||||||
|
req = req.header("Authorization", &creds.session);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
let result = req.send().await;
|
let result = req.send().await;
|
||||||
match result {
|
match result {
|
||||||
Ok(x) => {
|
Ok(x) => {
|
||||||
@ -163,6 +192,7 @@ pub async fn fetch_mirrors(
|
|||||||
mirrors: &[&str],
|
mirrors: &[&str],
|
||||||
sha1: Option<&str>,
|
sha1: Option<&str>,
|
||||||
semaphore: &FetchSemaphore,
|
semaphore: &FetchSemaphore,
|
||||||
|
credentials: &CredentialsStore,
|
||||||
) -> crate::Result<Bytes> {
|
) -> crate::Result<Bytes> {
|
||||||
if mirrors.is_empty() {
|
if mirrors.is_empty() {
|
||||||
return Err(crate::ErrorKind::InputError(
|
return Err(crate::ErrorKind::InputError(
|
||||||
@ -172,7 +202,7 @@ pub async fn fetch_mirrors(
|
|||||||
}
|
}
|
||||||
|
|
||||||
for (index, mirror) in mirrors.iter().enumerate() {
|
for (index, mirror) in mirrors.iter().enumerate() {
|
||||||
let result = fetch(mirror, sha1, semaphore).await;
|
let result = fetch(mirror, sha1, semaphore, credentials).await;
|
||||||
|
|
||||||
if result.is_ok() || (result.is_err() && index == (mirrors.len() - 1)) {
|
if result.is_ok() || (result.is_err() && index == (mirrors.len() - 1)) {
|
||||||
return result;
|
return result;
|
||||||
@ -186,7 +216,12 @@ pub async fn fetch_mirrors(
|
|||||||
#[tracing::instrument(skip(semaphore))]
|
#[tracing::instrument(skip(semaphore))]
|
||||||
#[theseus_macros::debug_pin]
|
#[theseus_macros::debug_pin]
|
||||||
pub async fn check_internet(semaphore: &FetchSemaphore, timeout: u64) -> bool {
|
pub async fn check_internet(semaphore: &FetchSemaphore, timeout: u64) -> bool {
|
||||||
let result = fetch("https://api.modrinth.com", None, semaphore);
|
let result = fetch(
|
||||||
|
"https://api.modrinth.com",
|
||||||
|
None,
|
||||||
|
semaphore,
|
||||||
|
&CredentialsStore(None),
|
||||||
|
);
|
||||||
let result =
|
let result =
|
||||||
tokio::time::timeout(Duration::from_secs(timeout), result).await;
|
tokio::time::timeout(Duration::from_secs(timeout), result).await;
|
||||||
matches!(result, Ok(Ok(_)))
|
matches!(result, Ok(Ok(_)))
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "theseus_cli"
|
name = "theseus_cli"
|
||||||
version = "0.3.1"
|
version = "0.4.0"
|
||||||
authors = ["Jai A <jaiagr+gpg@pm.me>"]
|
authors = ["Jai A <jaiagr+gpg@pm.me>"]
|
||||||
edition = "2018"
|
edition = "2018"
|
||||||
|
|
||||||
|
|||||||
@ -52,7 +52,7 @@ impl UserAdd {
|
|||||||
|
|
||||||
let credentials = flow.await??;
|
let credentials = flow.await??;
|
||||||
State::sync().await?;
|
State::sync().await?;
|
||||||
success!("Logged in user {}.", credentials.username);
|
success!("Logged in user {}.", credentials.0.username);
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "theseus_gui",
|
"name": "theseus_gui",
|
||||||
"private": true,
|
"private": true,
|
||||||
"version": "0.3.1",
|
"version": "0.4.0",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "vite",
|
"dev": "vite",
|
||||||
|
|||||||
6
theseus_gui/pnpm-lock.yaml
generated
6
theseus_gui/pnpm-lock.yaml
generated
@ -1,4 +1,8 @@
|
|||||||
lockfileVersion: '6.0'
|
lockfileVersion: '6.1'
|
||||||
|
|
||||||
|
settings:
|
||||||
|
autoInstallPeers: true
|
||||||
|
excludeLinksFromLockfile: false
|
||||||
|
|
||||||
dependencies:
|
dependencies:
|
||||||
'@tauri-apps/api':
|
'@tauri-apps/api':
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "theseus_gui"
|
name = "theseus_gui"
|
||||||
version = "0.3.1"
|
version = "0.4.0"
|
||||||
description = "A Tauri App"
|
description = "A Tauri App"
|
||||||
authors = ["you"]
|
authors = ["you"]
|
||||||
license = ""
|
license = ""
|
||||||
|
|||||||
@ -28,7 +28,8 @@ pub async fn auth_authenticate_begin_flow() -> Result<url::Url> {
|
|||||||
/// This completes the authentication flow quasi-synchronously, returning the sign-in credentials
|
/// This completes the authentication flow quasi-synchronously, returning the sign-in credentials
|
||||||
/// (and also adding the credentials to the state)
|
/// (and also adding the credentials to the state)
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
pub async fn auth_authenticate_await_completion() -> Result<Credentials> {
|
pub async fn auth_authenticate_await_completion(
|
||||||
|
) -> Result<(Credentials, Option<String>)> {
|
||||||
Ok(auth::authenticate_await_complete_flow().await?)
|
Ok(auth::authenticate_await_complete_flow().await?)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -7,6 +7,7 @@ pub mod import;
|
|||||||
pub mod jre;
|
pub mod jre;
|
||||||
pub mod logs;
|
pub mod logs;
|
||||||
pub mod metadata;
|
pub mod metadata;
|
||||||
|
pub mod mr_auth;
|
||||||
pub mod pack;
|
pub mod pack;
|
||||||
pub mod process;
|
pub mod process;
|
||||||
pub mod profile;
|
pub mod profile;
|
||||||
|
|||||||
88
theseus_gui/src-tauri/src/api/mr_auth.rs
Normal file
88
theseus_gui/src-tauri/src/api/mr_auth.rs
Normal file
@ -0,0 +1,88 @@
|
|||||||
|
use crate::api::Result;
|
||||||
|
use tauri::plugin::TauriPlugin;
|
||||||
|
use theseus::prelude::*;
|
||||||
|
|
||||||
|
pub fn init<R: tauri::Runtime>() -> TauriPlugin<R> {
|
||||||
|
tauri::plugin::Builder::new("mr_auth")
|
||||||
|
.invoke_handler(tauri::generate_handler![
|
||||||
|
authenticate_begin_flow,
|
||||||
|
authenticate_await_completion,
|
||||||
|
cancel_flow,
|
||||||
|
login_pass,
|
||||||
|
login_2fa,
|
||||||
|
login_minecraft,
|
||||||
|
create_account,
|
||||||
|
refresh,
|
||||||
|
logout,
|
||||||
|
get,
|
||||||
|
])
|
||||||
|
.build()
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tauri::command]
|
||||||
|
pub async fn authenticate_begin_flow(provider: &str) -> Result<String> {
|
||||||
|
Ok(theseus::mr_auth::authenticate_begin_flow(provider).await?)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tauri::command]
|
||||||
|
pub async fn authenticate_await_completion() -> Result<ModrinthCredentialsResult>
|
||||||
|
{
|
||||||
|
Ok(theseus::mr_auth::authenticate_await_complete_flow().await?)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tauri::command]
|
||||||
|
pub async fn cancel_flow() -> Result<()> {
|
||||||
|
Ok(theseus::mr_auth::cancel_flow().await?)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tauri::command]
|
||||||
|
pub async fn login_pass(
|
||||||
|
username: &str,
|
||||||
|
password: &str,
|
||||||
|
challenge: &str,
|
||||||
|
) -> Result<ModrinthCredentialsResult> {
|
||||||
|
Ok(theseus::mr_auth::login_password(username, password, challenge).await?)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tauri::command]
|
||||||
|
pub async fn login_2fa(code: &str, flow: &str) -> Result<ModrinthCredentials> {
|
||||||
|
Ok(theseus::mr_auth::login_2fa(code, flow).await?)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tauri::command]
|
||||||
|
pub async fn login_minecraft(flow: &str) -> Result<ModrinthCredentialsResult> {
|
||||||
|
Ok(theseus::mr_auth::login_minecraft(flow).await?)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tauri::command]
|
||||||
|
pub async fn create_account(
|
||||||
|
username: &str,
|
||||||
|
email: &str,
|
||||||
|
password: &str,
|
||||||
|
challenge: &str,
|
||||||
|
sign_up_newsletter: bool,
|
||||||
|
) -> Result<ModrinthCredentials> {
|
||||||
|
Ok(theseus::mr_auth::create_account(
|
||||||
|
username,
|
||||||
|
email,
|
||||||
|
password,
|
||||||
|
challenge,
|
||||||
|
sign_up_newsletter,
|
||||||
|
)
|
||||||
|
.await?)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tauri::command]
|
||||||
|
pub async fn refresh() -> Result<()> {
|
||||||
|
Ok(theseus::mr_auth::refresh().await?)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tauri::command]
|
||||||
|
pub async fn logout() -> Result<()> {
|
||||||
|
Ok(theseus::mr_auth::logout().await?)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tauri::command]
|
||||||
|
pub async fn get() -> Result<Option<ModrinthCredentials>> {
|
||||||
|
Ok(theseus::mr_auth::get_credentials().await?)
|
||||||
|
}
|
||||||
@ -127,6 +127,7 @@ fn main() {
|
|||||||
}
|
}
|
||||||
let builder = builder
|
let builder = builder
|
||||||
.plugin(api::auth::init())
|
.plugin(api::auth::init())
|
||||||
|
.plugin(api::mr_auth::init())
|
||||||
.plugin(api::import::init())
|
.plugin(api::import::init())
|
||||||
.plugin(api::logs::init())
|
.plugin(api::logs::init())
|
||||||
.plugin(api::jre::init())
|
.plugin(api::jre::init())
|
||||||
|
|||||||
@ -8,7 +8,7 @@
|
|||||||
},
|
},
|
||||||
"package": {
|
"package": {
|
||||||
"productName": "Modrinth App",
|
"productName": "Modrinth App",
|
||||||
"version": "0.3.1"
|
"version": "0.4.0"
|
||||||
},
|
},
|
||||||
"tauri": {
|
"tauri": {
|
||||||
"allowlist": {
|
"allowlist": {
|
||||||
|
|||||||
@ -54,14 +54,14 @@ const onboardingVideo = ref()
|
|||||||
defineExpose({
|
defineExpose({
|
||||||
initialize: async () => {
|
initialize: async () => {
|
||||||
isLoading.value = false
|
isLoading.value = false
|
||||||
const { theme, opt_out_analytics, collapsed_navigation, advanced_rendering, onboarded_new } =
|
const { theme, opt_out_analytics, collapsed_navigation, advanced_rendering, fully_onboarded } =
|
||||||
await get()
|
await get()
|
||||||
const os = await getOS()
|
const os = await getOS()
|
||||||
// video should play if the user is not on linux, and has not onboarded
|
// video should play if the user is not on linux, and has not onboarded
|
||||||
videoPlaying.value = !onboarded_new && os !== 'Linux'
|
videoPlaying.value = !fully_onboarded && os !== 'Linux'
|
||||||
const dev = await isDev()
|
const dev = await isDev()
|
||||||
const version = await getVersion()
|
const version = await getVersion()
|
||||||
showOnboarding.value = !onboarded_new
|
showOnboarding.value = !fully_onboarded
|
||||||
|
|
||||||
themeStore.setThemeState(theme)
|
themeStore.setThemeState(theme)
|
||||||
themeStore.collapsedNavigation = collapsed_navigation
|
themeStore.collapsedNavigation = collapsed_navigation
|
||||||
@ -71,7 +71,7 @@ defineExpose({
|
|||||||
if (opt_out_analytics) {
|
if (opt_out_analytics) {
|
||||||
mixpanel_opt_out_tracking()
|
mixpanel_opt_out_tracking()
|
||||||
}
|
}
|
||||||
mixpanel_track('Launched', { version, dev, onboarded_new })
|
mixpanel_track('Launched', { version, dev, fully_onboarded })
|
||||||
|
|
||||||
if (!dev) document.addEventListener('contextmenu', (event) => event.preventDefault())
|
if (!dev) document.addEventListener('contextmenu', (event) => event.preventDefault())
|
||||||
|
|
||||||
|
|||||||
@ -78,6 +78,7 @@ import {
|
|||||||
import { get, set } from '@/helpers/settings'
|
import { get, set } from '@/helpers/settings'
|
||||||
import { WebviewWindow } from '@tauri-apps/api/window'
|
import { WebviewWindow } from '@tauri-apps/api/window'
|
||||||
import { handleError } from '@/store/state.js'
|
import { handleError } from '@/store/state.js'
|
||||||
|
import { get as getCreds, login_minecraft } from '@/helpers/mr_auth'
|
||||||
import { mixpanel_track } from '@/helpers/mixpanel'
|
import { mixpanel_track } from '@/helpers/mixpanel'
|
||||||
|
|
||||||
defineProps({
|
defineProps({
|
||||||
@ -124,8 +125,20 @@ async function login() {
|
|||||||
})
|
})
|
||||||
|
|
||||||
const loggedIn = await authenticate_await_completion().catch(handleError)
|
const loggedIn = await authenticate_await_completion().catch(handleError)
|
||||||
await setAccount(loggedIn)
|
|
||||||
await refreshValues()
|
if (loggedIn && loggedIn[0]) {
|
||||||
|
await setAccount(loggedIn[0])
|
||||||
|
await refreshValues()
|
||||||
|
|
||||||
|
const creds = await getCreds().catch(handleError)
|
||||||
|
if (!creds) {
|
||||||
|
try {
|
||||||
|
await login_minecraft(loggedIn[1])
|
||||||
|
} catch (err) {
|
||||||
|
/* empty */
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
await window.close()
|
await window.close()
|
||||||
mixpanel_track('AccountLogIn')
|
mixpanel_track('AccountLogIn')
|
||||||
}
|
}
|
||||||
|
|||||||
@ -35,9 +35,10 @@ async function login() {
|
|||||||
|
|
||||||
const loggedIn = await authenticate_await_completion().catch(handleError)
|
const loggedIn = await authenticate_await_completion().catch(handleError)
|
||||||
loginModal.value.hide()
|
loginModal.value.hide()
|
||||||
props.nextPage()
|
|
||||||
|
props.nextPage(loggedIn[1])
|
||||||
const settings = await get().catch(handleError)
|
const settings = await get().catch(handleError)
|
||||||
settings.default_user = loggedIn.id
|
settings.default_user = loggedIn[0].id
|
||||||
await set(settings).catch(handleError)
|
await set(settings).catch(handleError)
|
||||||
await mixpanel.track('AccountLogIn')
|
await mixpanel.track('AccountLogIn')
|
||||||
}
|
}
|
||||||
@ -83,7 +84,7 @@ const openUrl = async () => {
|
|||||||
Browser didn't open?
|
Browser didn't open?
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
<Button class="transparent" large @click="nextPage"> Next </Button>
|
<Button class="transparent" large @click="nextPage()"> Next </Button>
|
||||||
</div>
|
</div>
|
||||||
</Card>
|
</Card>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -1,5 +1,5 @@
|
|||||||
<script setup>
|
<script setup>
|
||||||
import { Button, Card, UserIcon, LockIcon } from 'omorphia'
|
import { Button, Card, UserIcon, LockIcon, MailIcon, Checkbox } from 'omorphia'
|
||||||
import {
|
import {
|
||||||
DiscordIcon,
|
DiscordIcon,
|
||||||
GithubIcon,
|
GithubIcon,
|
||||||
@ -8,8 +8,19 @@ import {
|
|||||||
SteamIcon,
|
SteamIcon,
|
||||||
GitLabIcon,
|
GitLabIcon,
|
||||||
} from '@/assets/external'
|
} from '@/assets/external'
|
||||||
|
import {
|
||||||
|
authenticate_begin_flow,
|
||||||
|
authenticate_await_completion,
|
||||||
|
login_2fa,
|
||||||
|
create_account,
|
||||||
|
login_pass,
|
||||||
|
get as getCreds,
|
||||||
|
login_minecraft,
|
||||||
|
} from '@/helpers/mr_auth.js'
|
||||||
|
import { handleError, useNotifications } from '@/store/state.js'
|
||||||
|
import { onMounted, ref } from 'vue'
|
||||||
|
|
||||||
defineProps({
|
const props = defineProps({
|
||||||
nextPage: {
|
nextPage: {
|
||||||
type: Function,
|
type: Function,
|
||||||
required: true,
|
required: true,
|
||||||
@ -18,58 +29,201 @@ defineProps({
|
|||||||
type: Function,
|
type: Function,
|
||||||
required: true,
|
required: true,
|
||||||
},
|
},
|
||||||
|
modal: {
|
||||||
|
type: Boolean,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
flow: {
|
||||||
|
type: String,
|
||||||
|
default: null,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
const loggingIn = ref(true)
|
||||||
|
const twoFactorFlow = ref(null)
|
||||||
|
const twoFactorCode = ref('')
|
||||||
|
|
||||||
|
const email = ref('')
|
||||||
|
const username = ref('')
|
||||||
|
const password = ref('')
|
||||||
|
const confirmPassword = ref('')
|
||||||
|
const subscribe = ref(true)
|
||||||
|
|
||||||
|
async function signInOauth(provider) {
|
||||||
|
const url = await authenticate_begin_flow(provider).catch(handleError)
|
||||||
|
|
||||||
|
await window.__TAURI_INVOKE__('tauri', {
|
||||||
|
__tauriModule: 'Shell',
|
||||||
|
message: {
|
||||||
|
cmd: 'open',
|
||||||
|
path: url,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
const creds = await authenticate_await_completion().catch(handleError)
|
||||||
|
|
||||||
|
if (creds && creds.type === 'two_factor_required') {
|
||||||
|
twoFactorFlow.value = creds.flow
|
||||||
|
} else if (creds && creds.session) {
|
||||||
|
props.nextPage()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function signIn2fa() {
|
||||||
|
const creds = await login_2fa(twoFactorCode.value, twoFactorFlow.value).catch(handleError)
|
||||||
|
|
||||||
|
if (creds && creds.session) {
|
||||||
|
props.nextPage()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function signIn() {
|
||||||
|
const creds = await login_pass(
|
||||||
|
username.value,
|
||||||
|
password.value,
|
||||||
|
window.turnstile.getResponse()
|
||||||
|
).catch(handleError)
|
||||||
|
window.turnstile.reset()
|
||||||
|
|
||||||
|
if (creds && creds.type === 'two_factor_required') {
|
||||||
|
twoFactorFlow.value = creds.flow
|
||||||
|
} else if (creds && creds.session) {
|
||||||
|
props.nextPage()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function createAccount() {
|
||||||
|
if (password.value !== confirmPassword.value) {
|
||||||
|
const notifs = useNotifications()
|
||||||
|
notifs.addNotification({
|
||||||
|
title: 'An error occurred',
|
||||||
|
text: 'Passwords do not match!',
|
||||||
|
type: 'error',
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const creds = await create_account(
|
||||||
|
username.value,
|
||||||
|
email.value,
|
||||||
|
password.value,
|
||||||
|
window.turnstile.getResponse(),
|
||||||
|
subscribe.value
|
||||||
|
).catch(handleError)
|
||||||
|
window.turnstile.reset()
|
||||||
|
|
||||||
|
if (creds && creds.session) {
|
||||||
|
props.nextPage()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function goToNextPage() {
|
||||||
|
const creds = await getCreds().catch(handleError)
|
||||||
|
if (!creds) {
|
||||||
|
try {
|
||||||
|
await login_minecraft(props.flow)
|
||||||
|
} catch {
|
||||||
|
/* empty */
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
props.nextPage()
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
if (window.turnstile === null || !window.turnstile) {
|
||||||
|
const script = document.createElement('script')
|
||||||
|
script.src = 'https://challenges.cloudflare.com/turnstile/v0/api.js'
|
||||||
|
script.async = true
|
||||||
|
script.defer = true
|
||||||
|
document.head.appendChild(script)
|
||||||
|
}
|
||||||
})
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<Card>
|
<Card>
|
||||||
<h1>Login to Modrinth</h1>
|
<div class="cf-turnstile" data-sitekey="0x4AAAAAAAHWfmKCm7cUG869"></div>
|
||||||
<div class="button-grid">
|
<template v-if="twoFactorFlow">
|
||||||
<Button class="discord" large>
|
<h1>Enter two-factor code</h1>
|
||||||
<DiscordIcon />
|
<p>Please enter a two-factor code to proceed.</p>
|
||||||
Discord
|
<input v-model="twoFactorCode" maxlength="11" type="text" placeholder="Enter code..." />
|
||||||
</Button>
|
</template>
|
||||||
<Button class="github" large>
|
<template v-else>
|
||||||
<GithubIcon />
|
<h1 v-if="loggingIn">Login to Modrinth</h1>
|
||||||
Github
|
<h1 v-else>Create an account</h1>
|
||||||
</Button>
|
<div class="button-grid">
|
||||||
<Button class="white" large>
|
<Button class="discord" large @click="signInOauth('discord')">
|
||||||
<MicrosoftIcon />
|
<DiscordIcon />
|
||||||
Microsoft
|
Discord
|
||||||
</Button>
|
</Button>
|
||||||
<Button class="google" large>
|
<Button class="github" large @click="signInOauth('github')">
|
||||||
<GoogleIcon />
|
<GithubIcon />
|
||||||
Google
|
Github
|
||||||
</Button>
|
</Button>
|
||||||
<Button class="white" large>
|
<Button class="white" large @click="signInOauth('microsoft')">
|
||||||
<SteamIcon />
|
<MicrosoftIcon />
|
||||||
Steam
|
Microsoft
|
||||||
</Button>
|
</Button>
|
||||||
<Button class="gitlab" large>
|
<Button class="google" large @click="signInOauth('google')">
|
||||||
<GitLabIcon />
|
<GoogleIcon />
|
||||||
GitLab
|
Google
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
<Button class="white" large @click="signInOauth('steam')">
|
||||||
<div class="divider">
|
<SteamIcon />
|
||||||
<hr />
|
Steam
|
||||||
<p>Or</p>
|
</Button>
|
||||||
</div>
|
<Button class="gitlab" large @click="signInOauth('gitlab')">
|
||||||
<div class="iconified-input username">
|
<GitLabIcon />
|
||||||
<UserIcon />
|
GitLab
|
||||||
<input type="text" placeholder="Email or username" />
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
<div class="iconified-input">
|
<div class="divider">
|
||||||
<LockIcon />
|
<hr />
|
||||||
<input type="password" placeholder="Password" />
|
<p>Or</p>
|
||||||
</div>
|
</div>
|
||||||
<div class="link-row">
|
<div v-if="!loggingIn" class="iconified-input username">
|
||||||
<a class="button-base"> Create account </a>
|
<MailIcon />
|
||||||
<a class="button-base"> Forgot password? </a>
|
<input v-model="email" type="text" placeholder="Email" />
|
||||||
</div>
|
</div>
|
||||||
|
<div class="iconified-input username">
|
||||||
|
<UserIcon />
|
||||||
|
<input
|
||||||
|
v-model="username"
|
||||||
|
type="text"
|
||||||
|
:placeholder="loggingIn ? 'Email or username' : 'Username'"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div class="iconified-input" :class="{ username: !loggingIn }">
|
||||||
|
<LockIcon />
|
||||||
|
<input v-model="password" type="password" placeholder="Password" />
|
||||||
|
</div>
|
||||||
|
<div v-if="!loggingIn" class="iconified-input username">
|
||||||
|
<LockIcon />
|
||||||
|
<input v-model="confirmPassword" type="password" placeholder="Confirm password" />
|
||||||
|
</div>
|
||||||
|
<Checkbox
|
||||||
|
v-if="!loggingIn"
|
||||||
|
v-model="subscribe"
|
||||||
|
class="subscribe-btn"
|
||||||
|
label="Subscribe to updates about Modrinth"
|
||||||
|
/>
|
||||||
|
<div class="link-row">
|
||||||
|
<a v-if="loggingIn" class="button-base" @click="loggingIn = false"> Create account </a>
|
||||||
|
<a v-else class="button-base" @click="loggingIn = true">Sign in</a>
|
||||||
|
<a class="button-base" href="https://staging.modrinth.com/auth/reset-password">
|
||||||
|
Forgot password?
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
<div class="button-row">
|
<div class="button-row">
|
||||||
<Button class="transparent" large @click="prevPage"> Back </Button>
|
<Button class="transparent" large @click="prevPage"> {{ modal ? 'Close' : 'Back' }} </Button>
|
||||||
<Button color="primary" large> Login </Button>
|
<Button v-if="twoFactorCode" color="primary" large @click="signIn2fa"> Login </Button>
|
||||||
<Button class="transparent" large @click="nextPage"> Next </Button>
|
<Button v-else-if="loggingIn" color="primary" large @click="signIn"> Login </Button>
|
||||||
|
<Button v-else color="primary" large @click="createAccount"> Create account </Button>
|
||||||
|
<Button class="transparent" large @click="goToNextPage">
|
||||||
|
{{ modal ? 'Continue' : 'Next' }}
|
||||||
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</Card>
|
</Card>
|
||||||
</template>
|
</template>
|
||||||
@ -179,4 +333,10 @@ defineProps({
|
|||||||
padding: var(--gap-md) 0;
|
padding: var(--gap-md) 0;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
:deep {
|
||||||
|
.checkbox {
|
||||||
|
border: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
File diff suppressed because one or more lines are too long
@ -32,7 +32,7 @@ import StickyTitleBar from '@/components/ui/tutorial/StickyTitleBar.vue'
|
|||||||
import { auto_install_java, get_jre } from '@/helpers/jre.js'
|
import { auto_install_java, get_jre } from '@/helpers/jre.js'
|
||||||
import { handleError } from '@/store/notifications.js'
|
import { handleError } from '@/store/notifications.js'
|
||||||
import ImportingCard from '@/components/ui/tutorial/ImportingCard.vue'
|
import ImportingCard from '@/components/ui/tutorial/ImportingCard.vue'
|
||||||
// import ModrinthLoginScreen from '@/components/ui/tutorial/ModrinthLoginScreen.vue'
|
import ModrinthLoginScreen from '@/components/ui/tutorial/ModrinthLoginScreen.vue'
|
||||||
import PreImportScreen from '@/components/ui/tutorial/PreImportScreen.vue'
|
import PreImportScreen from '@/components/ui/tutorial/PreImportScreen.vue'
|
||||||
|
|
||||||
const phase = ref(0)
|
const phase = ref(0)
|
||||||
@ -45,6 +45,8 @@ const props = defineProps({
|
|||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
|
const flow = ref('')
|
||||||
|
|
||||||
const nextPhase = () => {
|
const nextPhase = () => {
|
||||||
phase.value++
|
phase.value++
|
||||||
mixpanel.track('TutorialPhase', { page: phase.value })
|
mixpanel.track('TutorialPhase', { page: phase.value })
|
||||||
@ -54,9 +56,13 @@ const prevPhase = () => {
|
|||||||
phase.value--
|
phase.value--
|
||||||
}
|
}
|
||||||
|
|
||||||
const nextPage = () => {
|
const nextPage = (newFlow) => {
|
||||||
page.value++
|
page.value++
|
||||||
mixpanel.track('OnboardingPage', { page: page.value })
|
mixpanel.track('OnboardingPage', { page: page.value })
|
||||||
|
|
||||||
|
if (newFlow) {
|
||||||
|
flow.value = newFlow
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const endOnboarding = () => {
|
const endOnboarding = () => {
|
||||||
@ -70,7 +76,7 @@ const prevPage = () => {
|
|||||||
const finishOnboarding = async () => {
|
const finishOnboarding = async () => {
|
||||||
mixpanel.track('OnboardingFinish')
|
mixpanel.track('OnboardingFinish')
|
||||||
const settings = await get()
|
const settings = await get()
|
||||||
settings.onboarded_new = true
|
settings.fully_onboarded = true
|
||||||
await set(settings)
|
await set(settings)
|
||||||
props.finish()
|
props.finish()
|
||||||
}
|
}
|
||||||
@ -119,14 +125,20 @@ onMounted(async () => {
|
|||||||
<Button color="primary" @click="nextPage"> Get started </Button>
|
<Button color="primary" @click="nextPage"> Get started </Button>
|
||||||
</GalleryImage>
|
</GalleryImage>
|
||||||
<LoginCard v-else-if="page === 2" :next-page="nextPage" :prev-page="prevPage" />
|
<LoginCard v-else-if="page === 2" :next-page="nextPage" :prev-page="prevPage" />
|
||||||
<!-- <ModrinthLoginScreen v-else-if="page === 3" :next-page="nextPage" :prev-page="prevPage" />-->
|
<ModrinthLoginScreen
|
||||||
<PreImportScreen
|
|
||||||
v-else-if="page === 3"
|
v-else-if="page === 3"
|
||||||
|
:modal="false"
|
||||||
|
:next-page="nextPage"
|
||||||
|
:prev-page="prevPage"
|
||||||
|
:flow="flow"
|
||||||
|
/>
|
||||||
|
<PreImportScreen
|
||||||
|
v-else-if="page === 4"
|
||||||
:next-page="endOnboarding"
|
:next-page="endOnboarding"
|
||||||
:prev-page="prevPage"
|
:prev-page="prevPage"
|
||||||
:import-page="nextPage"
|
:import-page="nextPage"
|
||||||
/>
|
/>
|
||||||
<ImportingCard v-else-if="page === 4" :next-page="endOnboarding" :prev-page="prevPage" />
|
<ImportingCard v-else-if="page === 5" :next-page="endOnboarding" :prev-page="prevPage" />
|
||||||
</div>
|
</div>
|
||||||
<div v-else class="container">
|
<div v-else class="container">
|
||||||
<StickyTitleBar v-if="phase === 9" />
|
<StickyTitleBar v-if="phase === 9" />
|
||||||
|
|||||||
50
theseus_gui/src/helpers/mr_auth.js
Normal file
50
theseus_gui/src/helpers/mr_auth.js
Normal file
@ -0,0 +1,50 @@
|
|||||||
|
/**
|
||||||
|
* All theseus API calls return serialized values (both return values and errors);
|
||||||
|
* So, for example, addDefaultInstance creates a blank Profile object, where the Rust struct is serialized,
|
||||||
|
* and deserialized into a usable JS object.
|
||||||
|
*/
|
||||||
|
import { invoke } from '@tauri-apps/api/tauri'
|
||||||
|
|
||||||
|
export async function authenticate_begin_flow(provider) {
|
||||||
|
return await invoke('plugin:mr_auth|authenticate_begin_flow', { provider })
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function authenticate_await_completion() {
|
||||||
|
return await invoke('plugin:mr_auth|authenticate_await_completion')
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function cancel_flow() {
|
||||||
|
return await invoke('plugin:mr_auth|cancel_flow')
|
||||||
|
}
|
||||||
|
export async function login_pass(username, password, challenge) {
|
||||||
|
return await invoke('plugin:mr_auth|login_pass', { username, password, challenge })
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function login_2fa(code, flow) {
|
||||||
|
return await invoke('plugin:mr_auth|login_2fa', { code, flow })
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function login_minecraft(flow) {
|
||||||
|
return await invoke('plugin:mr_auth|login_minecraft', { flow })
|
||||||
|
}
|
||||||
|
export async function create_account(username, email, password, challenge, signUpNewsletter) {
|
||||||
|
return await invoke('plugin:mr_auth|create_account', {
|
||||||
|
username,
|
||||||
|
email,
|
||||||
|
password,
|
||||||
|
challenge,
|
||||||
|
signUpNewsletter,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function refresh() {
|
||||||
|
return await invoke('plugin:mr_auth|refresh')
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function logout() {
|
||||||
|
return await invoke('plugin:mr_auth|logout')
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function get() {
|
||||||
|
return await invoke('plugin:mr_auth|get')
|
||||||
|
}
|
||||||
@ -1,10 +1,12 @@
|
|||||||
<script setup>
|
<script setup>
|
||||||
import { ref, watch } from 'vue'
|
import { ref, watch } from 'vue'
|
||||||
import { Card, Slider, DropdownSelect, Toggle } from 'omorphia'
|
import { Card, Slider, DropdownSelect, Toggle, Modal, LogOutIcon, LogInIcon } from 'omorphia'
|
||||||
import { handleError, useTheming } from '@/store/state'
|
import { handleError, useTheming } from '@/store/state'
|
||||||
import { get, set } from '@/helpers/settings'
|
import { get, set } from '@/helpers/settings'
|
||||||
import { get_max_memory } from '@/helpers/jre'
|
import { get_max_memory } from '@/helpers/jre'
|
||||||
|
import { get as getCreds, logout } from '@/helpers/mr_auth.js'
|
||||||
import JavaSelector from '@/components/ui/JavaSelector.vue'
|
import JavaSelector from '@/components/ui/JavaSelector.vue'
|
||||||
|
import ModrinthLoginScreen from '@/components/ui/tutorial/ModrinthLoginScreen.vue'
|
||||||
import { mixpanel_opt_out_tracking, mixpanel_opt_in_tracking } from '@/helpers/mixpanel'
|
import { mixpanel_opt_out_tracking, mixpanel_opt_in_tracking } from '@/helpers/mixpanel'
|
||||||
|
|
||||||
const pageOptions = ['Home', 'Library']
|
const pageOptions = ['Home', 'Library']
|
||||||
@ -76,10 +78,54 @@ watch(
|
|||||||
},
|
},
|
||||||
{ deep: true }
|
{ deep: true }
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const credentials = ref(await getCreds().catch(handleError))
|
||||||
|
const loginScreenModal = ref()
|
||||||
|
|
||||||
|
async function logOut() {
|
||||||
|
await logout().catch(handleError)
|
||||||
|
credentials.value = await getCreds().catch(handleError)
|
||||||
|
}
|
||||||
|
|
||||||
|
async function signInAfter() {
|
||||||
|
loginScreenModal.value.hide()
|
||||||
|
credentials.value = await getCreds().catch(handleError)
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="settings-page">
|
<div class="settings-page">
|
||||||
|
<Card>
|
||||||
|
<div class="label">
|
||||||
|
<h3>
|
||||||
|
<span class="label__title size-card-header">Account</span>
|
||||||
|
</h3>
|
||||||
|
</div>
|
||||||
|
<Modal
|
||||||
|
ref="loginScreenModal"
|
||||||
|
class="login-screen-modal"
|
||||||
|
:noblur="!themeStore.advancedRendering"
|
||||||
|
>
|
||||||
|
<ModrinthLoginScreen
|
||||||
|
:modal="true"
|
||||||
|
:prev-page="$refs.loginScreenModal.show()"
|
||||||
|
:next-page="signInAfter"
|
||||||
|
/>
|
||||||
|
</Modal>
|
||||||
|
<div class="adjacent-input">
|
||||||
|
<label for="theme">
|
||||||
|
<span class="label__title">Manage account</span>
|
||||||
|
<span v-if="credentials" class="label__description">
|
||||||
|
You are currently logged in as {{ credentials.user.username }}.
|
||||||
|
</span>
|
||||||
|
<span v-else> Sign in to your Modrinth account. </span>
|
||||||
|
</label>
|
||||||
|
<button v-if="credentials" class="btn" @click="logOut"><LogOutIcon /> Sign out</button>
|
||||||
|
<button v-else class="btn" @click="$refs.loginScreenModal.show()">
|
||||||
|
<LogInIcon /> Sign in
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</Card>
|
||||||
<Card>
|
<Card>
|
||||||
<div class="label">
|
<div class="label">
|
||||||
<h3>
|
<h3>
|
||||||
@ -397,4 +443,16 @@ watch(
|
|||||||
.card-divider {
|
.card-divider {
|
||||||
margin: 1rem 0;
|
margin: 1rem 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
:deep {
|
||||||
|
.login-screen-modal {
|
||||||
|
.modal-container .modal-body {
|
||||||
|
width: auto;
|
||||||
|
|
||||||
|
.content {
|
||||||
|
background: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@ -24,8 +24,8 @@ pub async fn authenticate_run() -> theseus::Result<Credentials> {
|
|||||||
let credentials = auth::authenticate_await_complete_flow().await?;
|
let credentials = auth::authenticate_await_complete_flow().await?;
|
||||||
State::sync().await?;
|
State::sync().await?;
|
||||||
|
|
||||||
println!("Logged in user {}.", credentials.username);
|
println!("Logged in user {}.", credentials.0.username);
|
||||||
Ok(credentials)
|
Ok(credentials.0)
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tokio::main]
|
#[tokio::main]
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user