You've already forked AstralRinth
forked from didirus/AstralRinth
Migrate to SQLite for Internal Launcher Data (#1300)
* initial migration * barebones profiles * Finish profiles * Add back file watcher * UI support progress * Finish most of cache * Fix options page * Fix forge, finish modrinth auth * Accounts, process cache * Run SQLX prepare * Finish * Run lint + actions * Fix version to be compat with windows * fix lint * actually fix lint * actually fix lint again
This commit is contained in:
@@ -1,37 +1,180 @@
|
||||
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 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;
|
||||
|
||||
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,
|
||||
pub expires: DateTime<Utc>,
|
||||
pub user_id: String,
|
||||
pub active: bool,
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
impl ModrinthCredentials {
|
||||
pub async fn get_and_refresh(
|
||||
exec: impl sqlx::Executor<'_, Database = sqlx::Sqlite> + Copy,
|
||||
semaphore: &FetchSemaphore,
|
||||
) -> crate::Result<Option<Self>> {
|
||||
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::<Session>(&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<Option<Self>> {
|
||||
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<DashMap<String, Self>> {
|
||||
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 {
|
||||
@@ -39,159 +182,25 @@ pub enum ModrinthCredentialsResult {
|
||||
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,
|
||||
exec: impl sqlx::Executor<'_, Database = sqlx::Sqlite>,
|
||||
) -> 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?;
|
||||
let info = fetch_info(code, semaphore, exec).await?;
|
||||
|
||||
Ok(ModrinthCredentialsResult::Credentials(
|
||||
ModrinthCredentials {
|
||||
session: code.to_string(),
|
||||
expires_at: Utc::now() + Duration::weeks(2),
|
||||
user: info,
|
||||
expires: Utc::now() + Duration::weeks(2),
|
||||
user_id: info.id,
|
||||
active: true,
|
||||
},
|
||||
))
|
||||
} else if let Some(error) =
|
||||
@@ -209,48 +218,19 @@ async fn get_result_from_res(
|
||||
}
|
||||
}
|
||||
|
||||
#[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!("{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,
|
||||
exec: impl sqlx::Executor<'_, Database = sqlx::Sqlite>,
|
||||
) -> crate::Result<ModrinthCredentials> {
|
||||
if let Some(code) = response.get("session").and_then(|x| x.as_str()) {
|
||||
let info = fetch_info(code, semaphore).await?;
|
||||
let info = fetch_info(code, semaphore, exec).await?;
|
||||
|
||||
Ok(ModrinthCredentials {
|
||||
session: code.to_string(),
|
||||
expires_at: Utc::now() + Duration::weeks(2),
|
||||
user: info,
|
||||
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())
|
||||
@@ -267,10 +247,53 @@ async fn get_creds_from_res(
|
||||
}
|
||||
}
|
||||
|
||||
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<String, Value>,
|
||||
semaphore: &FetchSemaphore,
|
||||
exec: impl sqlx::Executor<'_, Database = sqlx::Sqlite>,
|
||||
) -> crate::Result<ModrinthCredentialsResult> {
|
||||
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<ModrinthCredentialsResult> {
|
||||
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::<HashMap<String, Value>>(&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<ModrinthCredentials> {
|
||||
let resp = fetch_advanced(
|
||||
Method::POST,
|
||||
@@ -283,13 +306,13 @@ pub async fn login_2fa(
|
||||
None,
|
||||
None,
|
||||
semaphore,
|
||||
&CredentialsStore(None),
|
||||
exec,
|
||||
)
|
||||
.await?;
|
||||
|
||||
let response = serde_json::from_slice::<HashMap<String, Value>>(&resp)?;
|
||||
|
||||
get_creds_from_res(response, semaphore).await
|
||||
get_creds_from_res(response, semaphore, exec).await
|
||||
}
|
||||
|
||||
pub async fn create_account(
|
||||
@@ -299,6 +322,7 @@ pub async fn create_account(
|
||||
challenge: &str,
|
||||
sign_up_newsletter: bool,
|
||||
semaphore: &FetchSemaphore,
|
||||
exec: impl sqlx::Executor<'_, Database = sqlx::Sqlite> + Copy,
|
||||
) -> crate::Result<ModrinthCredentials> {
|
||||
let resp = fetch_advanced(
|
||||
Method::POST,
|
||||
@@ -314,51 +338,19 @@ pub async fn create_account(
|
||||
None,
|
||||
None,
|
||||
semaphore,
|
||||
&CredentialsStore(None),
|
||||
exec,
|
||||
)
|
||||
.await?;
|
||||
let response = serde_json::from_slice::<HashMap<String, Value>>(&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::<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;
|
||||
}
|
||||
}
|
||||
|
||||
credentials_store.save().await?;
|
||||
Ok(())
|
||||
get_creds_from_res(response, semaphore, exec).await
|
||||
}
|
||||
|
||||
async fn fetch_info(
|
||||
token: &str,
|
||||
semaphore: &FetchSemaphore,
|
||||
) -> crate::Result<ModrinthUser> {
|
||||
exec: impl sqlx::Executor<'_, Database = sqlx::Sqlite>,
|
||||
) -> crate::Result<crate::state::cache::User> {
|
||||
let result = fetch_advanced(
|
||||
Method::GET,
|
||||
&format!("{MODRINTH_API_URL}user"),
|
||||
@@ -367,7 +359,7 @@ async fn fetch_info(
|
||||
Some(("Authorization", token)),
|
||||
None,
|
||||
semaphore,
|
||||
&CredentialsStore(None),
|
||||
exec,
|
||||
)
|
||||
.await?;
|
||||
let value = serde_json::from_slice(&result)?;
|
||||
|
||||
Reference in New Issue
Block a user