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, pub avatar_url: Option, pub bio: Option, pub created: DateTime, pub role: String, } #[derive(Serialize, Deserialize, Clone, Debug)] pub struct ModrinthCredentials { pub session: String, pub expires_at: DateTime, 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); impl CredentialsStore { pub async fn init( dirs: &DirectoryInfo, io_semaphore: &IoSemaphore, ) -> crate::Result { 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 { 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 { 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::(&code_resp)?; Ok(response.url) } pub async fn extract_credentials( &mut self, semaphore: &FetchSemaphore, ) -> crate::Result { // 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::>(&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, semaphore: &FetchSemaphore, ) -> crate::Result { 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 { let resp = fetch_advanced( Method::POST, &format!("{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::>(&resp)?; get_result_from_res("session", value, semaphore).await } async fn get_creds_from_res( response: HashMap, semaphore: &FetchSemaphore, ) -> crate::Result { 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 { 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::>(&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 { 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::>(&resp)?; get_creds_from_res(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::(&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 { 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) }