You've already forked AstralRinth
forked from didirus/AstralRinth
Auth bindings (#58)
* basic framework. still has errors * added functionality for main endpoints + some structuring * formatting * unused code * mimicked CLI function with wait_for process * added basic auth bindings * made PR changes, added playground * cargo fmt * removed missed println * misc tests fixes * cargo fmt * added windows support * cargo fmt * all OS use dunce * restructured profile slightly; fixed mac bug * profile changes, new main.rs * fixed requested pr + canonicaliation bug * fixed regressed bug in ui * fixed regressed bugs * fixed git error * typo * ran prettier * clippy * playground clippy * ported profile loading fix * profile change for real, url println and clippy * PR changes * auth bindings + semisynch flow * fixed dropping task error * prettier, eslint, clippy * removed debugging modifications * removed unused function that eslinter missed :( * fixed settings not being released --------- Co-authored-by: Wyatt <wyatt@modrinth.com>
This commit is contained in:
@@ -5,6 +5,28 @@ use tokio::sync::oneshot;
|
||||
|
||||
pub use inner::Credentials;
|
||||
|
||||
/// Authenticate a user with Hydra - part 1
|
||||
/// This begins the authentication flow quasi-synchronously, returning a URL
|
||||
/// This can be used in conjunction with 'authenticate_await_complete_flow'
|
||||
/// to call authenticate and call the flow from the frontend.
|
||||
/// Visit the URL in a browser, then call and await 'authenticate_await_complete_flow'.
|
||||
pub async fn authenticate_begin_flow() -> crate::Result<url::Url> {
|
||||
let st = State::get().await?.clone();
|
||||
let url = st.auth_flow.write().await.begin_auth().await?;
|
||||
Ok(url)
|
||||
}
|
||||
|
||||
/// Authenticate a user with Hydra - part 2
|
||||
/// This completes the authentication flow quasi-synchronously, returning the credentials
|
||||
/// This can be used in conjunction with 'authenticate_begin_flow'
|
||||
/// to call authenticate and call the flow from the frontend.
|
||||
pub async fn authenticate_await_complete_flow() -> crate::Result<Credentials> {
|
||||
let st = State::get().await?.clone();
|
||||
let credentials =
|
||||
st.auth_flow.write().await.await_auth_completion().await?;
|
||||
Ok(credentials)
|
||||
}
|
||||
|
||||
/// Authenticate a user with Hydra
|
||||
/// To run this, you need to first spawn this function as a task, then
|
||||
/// open a browser to the given URL and finally wait on the spawned future
|
||||
@@ -36,6 +58,7 @@ pub async fn authenticate(
|
||||
}
|
||||
|
||||
/// Refresh some credentials using Hydra, if needed
|
||||
/// This is the primary desired way to get credentials, as it will also refresh them.
|
||||
#[tracing::instrument]
|
||||
pub async fn refresh(
|
||||
user: uuid::Uuid,
|
||||
@@ -98,3 +121,18 @@ pub async fn users() -> crate::Result<Box<[Credentials]>> {
|
||||
let users = state.users.read().await;
|
||||
users.iter().collect()
|
||||
}
|
||||
|
||||
/// Get a specific user by user ID
|
||||
/// Prefer to use 'refresh' instead of this function
|
||||
#[tracing::instrument]
|
||||
pub async fn get_user(user: uuid::Uuid) -> crate::Result<Credentials> {
|
||||
let state = State::get().await?;
|
||||
let users = state.users.read().await;
|
||||
let user = users.get(user)?.ok_or_else(|| {
|
||||
crate::ErrorKind::OtherError(format!(
|
||||
"Tried to get nonexistent user with ID {user}"
|
||||
))
|
||||
.as_error()
|
||||
})?;
|
||||
Ok(user)
|
||||
}
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
//! Theseus error type
|
||||
use tracing_error::InstrumentError;
|
||||
|
||||
use crate::profile_create;
|
||||
use tracing_error::InstrumentError;
|
||||
|
||||
#[derive(thiserror::Error, Debug)]
|
||||
pub enum ErrorKind {
|
||||
@@ -32,9 +31,12 @@ pub enum ErrorKind {
|
||||
#[error("Metadata error: {0}")]
|
||||
MetadataError(#[from] daedalus::Error),
|
||||
|
||||
#[error("Minecraft authentication error: {0}")]
|
||||
#[error("Minecraft authentication Hydra error: {0}")]
|
||||
HydraError(String),
|
||||
|
||||
#[error("Minecraft authentication task error: {0}")]
|
||||
AuthTaskError(#[from] crate::state::AuthTaskError),
|
||||
|
||||
#[error("I/O error: {0}")]
|
||||
IOError(#[from] std::io::Error),
|
||||
|
||||
@@ -59,6 +61,9 @@ pub enum ErrorKind {
|
||||
#[error("Invalid input: {0}")]
|
||||
InputError(String),
|
||||
|
||||
#[error("Join handle error: {0}")]
|
||||
JoinError(#[from] tokio::task::JoinError),
|
||||
|
||||
#[error("Recv error: {0}")]
|
||||
RecvError(#[from] tokio::sync::oneshot::error::RecvError),
|
||||
|
||||
|
||||
74
theseus/src/state/auth_task.rs
Normal file
74
theseus/src/state/auth_task.rs
Normal file
@@ -0,0 +1,74 @@
|
||||
use crate::launcher::auth::Credentials;
|
||||
use std::mem;
|
||||
use tokio::task::JoinHandle;
|
||||
|
||||
// Authentication task
|
||||
// A wrapper over the authentication task that allows it to be called from the frontend
|
||||
// without caching the task handle in the frontend
|
||||
|
||||
pub struct AuthTask(Option<JoinHandle<crate::Result<Credentials>>>);
|
||||
|
||||
impl AuthTask {
|
||||
pub fn new() -> AuthTask {
|
||||
AuthTask(None)
|
||||
}
|
||||
|
||||
pub async fn begin_auth(&mut self) -> crate::Result<url::Url> {
|
||||
// Creates a channel to receive the URL
|
||||
let (tx, rx) = tokio::sync::oneshot::channel::<url::Url>();
|
||||
let task = tokio::spawn(crate::auth::authenticate(tx));
|
||||
|
||||
// If receiver is dropped, try to get Hydra error
|
||||
let url = rx.await;
|
||||
let url = match url {
|
||||
Ok(url) => url,
|
||||
Err(e) => {
|
||||
task.await??;
|
||||
return Err(e.into()); // truly a dropped receiver
|
||||
}
|
||||
};
|
||||
|
||||
// Flow is going, store in state and return
|
||||
self.0 = Some(task);
|
||||
|
||||
Ok(url)
|
||||
}
|
||||
|
||||
pub async fn await_auth_completion(
|
||||
&mut self,
|
||||
) -> crate::Result<Credentials> {
|
||||
// Gets the task handle from the state, replacing with None
|
||||
let task = mem::replace(&mut self.0, None);
|
||||
|
||||
// Waits for the task to complete, and returns the credentials
|
||||
let credentials = task
|
||||
.ok_or_else(|| AuthTaskError::TaskMissing)?
|
||||
.await
|
||||
.map_err(AuthTaskError::from)??;
|
||||
|
||||
Ok(credentials)
|
||||
}
|
||||
|
||||
pub async fn cancel(&mut self) {
|
||||
// Gets the task handle from the state, replacing with None
|
||||
let task = mem::replace(&mut self.0, None);
|
||||
if let Some(task) = task {
|
||||
// Cancels the task
|
||||
task.abort();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for AuthTask {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(thiserror::Error, Debug)]
|
||||
pub enum AuthTaskError {
|
||||
#[error("Authentication task was aborted or missing")]
|
||||
TaskMissing,
|
||||
#[error("Join handle error")]
|
||||
JoinHandleError(#[from] tokio::task::JoinError),
|
||||
}
|
||||
@@ -25,6 +25,9 @@ pub use self::users::*;
|
||||
mod children;
|
||||
pub use self::children::*;
|
||||
|
||||
mod auth_task;
|
||||
pub use self::auth_task::*;
|
||||
|
||||
// Global state
|
||||
static LAUNCHER_STATE: OnceCell<Arc<State>> = OnceCell::const_new();
|
||||
pub struct State {
|
||||
@@ -39,8 +42,10 @@ pub struct State {
|
||||
// TODO: settings API
|
||||
/// Launcher configuration
|
||||
pub settings: RwLock<Settings>,
|
||||
/// Reference to process children
|
||||
/// Reference to minecraft process children
|
||||
pub children: RwLock<Children>,
|
||||
/// Authentication flow
|
||||
pub auth_flow: RwLock<AuthTask>,
|
||||
/// Launcher profile metadata
|
||||
pub(crate) profiles: RwLock<Profiles>,
|
||||
/// Launcher user account info
|
||||
@@ -80,6 +85,8 @@ impl State {
|
||||
|
||||
let children = Children::new();
|
||||
|
||||
let auth_flow = AuthTask::new();
|
||||
|
||||
Ok(Arc::new(Self {
|
||||
database,
|
||||
directories,
|
||||
@@ -89,6 +96,7 @@ impl State {
|
||||
profiles: RwLock::new(profiles),
|
||||
users: RwLock::new(users),
|
||||
children: RwLock::new(children),
|
||||
auth_flow: RwLock::new(auth_flow),
|
||||
}))
|
||||
}
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user