use crate::config::MODRINTH_API_URL; use crate::util::fetch::{fetch_advanced, FetchSemaphore}; use chrono::{DateTime, Duration, TimeZone, Utc}; use dashmap::DashMap; use futures::TryStreamExt; use reqwest::Method; use serde::{Deserialize, Serialize}; use serde_json::Value; use std::collections::HashMap; #[derive(Serialize, Deserialize, Clone, Debug)] pub struct ModrinthCredentials { pub session: String, pub expires: DateTime, pub user_id: String, pub active: bool, } impl ModrinthCredentials { pub async fn get_and_refresh( exec: impl sqlx::Executor<'_, Database = sqlx::Sqlite> + Copy, semaphore: &FetchSemaphore, ) -> crate::Result> { let creds = Self::get_active(exec).await?; if let Some(mut creds) = creds { if creds.expires < Utc::now() { #[derive(Deserialize)] struct Session { session: String, } let resp = fetch_advanced( Method::POST, &format!("{MODRINTH_API_URL}session/refresh"), None, None, Some(("Authorization", &*creds.session)), None, semaphore, exec, ) .await .ok() .and_then(|resp| serde_json::from_slice::(&resp).ok()); if let Some(value) = resp { creds.session = value.session; creds.expires = Utc::now() + Duration::weeks(2); creds.upsert(exec).await?; Ok(Some(creds)) } else { Self::remove(&creds.user_id, exec).await?; Ok(None) } } else { Ok(Some(creds)) } } else { Ok(None) } } pub async fn get_active( exec: impl sqlx::Executor<'_, Database = sqlx::Sqlite>, ) -> crate::Result> { let res = sqlx::query!( " SELECT id, active, session_id, expires FROM modrinth_users WHERE active = TRUE " ) .fetch_optional(exec) .await?; Ok(res.map(|x| Self { session: x.session_id, expires: Utc .timestamp_opt(x.expires, 0) .single() .unwrap_or_else(Utc::now), user_id: x.id, active: x.active == 1, })) } pub async fn get_all( exec: impl sqlx::Executor<'_, Database = sqlx::Sqlite>, ) -> crate::Result> { let res = sqlx::query!( " SELECT id, active, session_id, expires FROM modrinth_users " ) .fetch(exec) .try_fold(DashMap::new(), |acc, x| { acc.insert( x.id.clone(), Self { session: x.session_id, expires: Utc .timestamp_opt(x.expires, 0) .single() .unwrap_or_else(Utc::now), user_id: x.id, active: x.active == 1, }, ); async move { Ok(acc) } }) .await?; Ok(res) } pub async fn upsert( &self, exec: impl sqlx::Executor<'_, Database = sqlx::Sqlite> + Copy, ) -> crate::Result<()> { let expires = self.expires.timestamp(); if self.active { sqlx::query!( " UPDATE modrinth_users SET active = FALSE " ) .execute(exec) .await?; } sqlx::query!( " INSERT INTO modrinth_users (id, active, session_id, expires) VALUES ($1, $2, $3, $4) ON CONFLICT (id) DO UPDATE SET active = $2, session_id = $3, expires = $4 ", self.user_id, self.active, self.session, expires, ) .execute(exec) .await?; Ok(()) } pub async fn remove( user_id: &str, exec: impl sqlx::Executor<'_, Database = sqlx::Sqlite>, ) -> crate::Result<()> { sqlx::query!( " DELETE FROM modrinth_users WHERE id = $1 ", user_id, ) .execute(exec) .await?; Ok(()) } } #[derive(Serialize, Debug)] #[serde(tag = "type")] #[serde(rename_all = "snake_case")] pub enum ModrinthCredentialsResult { TwoFactorRequired { flow: String }, Credentials(ModrinthCredentials), } async fn get_result_from_res( code_key: &str, response: HashMap, semaphore: &FetchSemaphore, exec: impl sqlx::Executor<'_, Database = sqlx::Sqlite>, ) -> 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, exec).await?; Ok(ModrinthCredentialsResult::Credentials( ModrinthCredentials { session: code.to_string(), expires: Utc::now() + Duration::weeks(2), user_id: info.id, active: true, }, )) } 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()) } } async fn get_creds_from_res( response: HashMap, semaphore: &FetchSemaphore, exec: impl sqlx::Executor<'_, Database = sqlx::Sqlite>, ) -> crate::Result { if let Some(code) = response.get("session").and_then(|x| x.as_str()) { let info = fetch_info(code, semaphore, exec).await?; Ok(ModrinthCredentials { session: code.to_string(), expires: Utc::now() + Duration::weeks(2), user_id: info.id, active: true, }) } 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 fn get_login_url(provider: &str) -> String { format!( "{MODRINTH_API_URL}auth/init?url={}&provider={provider}", urlencoding::encode("https://launcher-files.modrinth.com/detect.txt") ) } pub async fn finish_login_flow( response: HashMap, semaphore: &FetchSemaphore, exec: impl sqlx::Executor<'_, Database = sqlx::Sqlite>, ) -> crate::Result { get_result_from_res("code", response, semaphore, exec).await } pub async fn login_password( username: &str, password: &str, challenge: &str, semaphore: &FetchSemaphore, exec: impl sqlx::Executor<'_, Database = sqlx::Sqlite> + Copy, ) -> 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, exec, ) .await?; let value = serde_json::from_slice::>(&resp)?; get_result_from_res("session", value, semaphore, exec).await } pub async fn login_2fa( code: &str, flow: &str, semaphore: &FetchSemaphore, exec: impl sqlx::Executor<'_, Database = sqlx::Sqlite> + Copy, ) -> 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, exec, ) .await?; let response = serde_json::from_slice::>(&resp)?; get_creds_from_res(response, semaphore, exec).await } pub async fn create_account( username: &str, email: &str, password: &str, challenge: &str, sign_up_newsletter: bool, semaphore: &FetchSemaphore, exec: impl sqlx::Executor<'_, Database = sqlx::Sqlite> + Copy, ) -> 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, exec, ) .await?; let response = serde_json::from_slice::>(&resp)?; get_creds_from_res(response, semaphore, exec).await } async fn fetch_info( token: &str, semaphore: &FetchSemaphore, exec: impl sqlx::Executor<'_, Database = sqlx::Sqlite>, ) -> crate::Result { let result = fetch_advanced( Method::GET, &format!("{MODRINTH_API_URL}user"), None, None, Some(("Authorization", token)), None, semaphore, exec, ) .await?; let value = serde_json::from_slice(&result)?; Ok(value) }