Authentication (#37)

* Initial authentication implementation

* Store user info in the database, improve encapsulation in profiles

* Add user list, remove unused dependencies, add spantraces

* Implement user remove, update UUID crate

* Add user set-default

* Revert submodule macro usage

* Make tracing significantly less verbose
This commit is contained in:
Danielle
2022-07-15 15:39:38 +00:00
committed by GitHub
parent 53948c7a5e
commit b223dc7cba
27 changed files with 1490 additions and 851 deletions

View File

@@ -1,26 +1,52 @@
use eyre::Result;
use futures::TryFutureExt;
use paris::*;
use tracing_error::ErrorLayer;
use tracing_futures::WithSubscriber;
use tracing_subscriber::prelude::*;
use tracing_subscriber::{fmt, EnvFilter};
mod subcommands;
#[macro_use]
mod util;
#[derive(argh::FromArgs)]
mod subcommands;
#[derive(argh::FromArgs, Debug)]
/// The official Modrinth CLI
pub struct Args {
#[argh(subcommand)]
pub subcommand: subcommands::SubCommand,
pub subcommand: subcommands::Subcommand,
}
#[tokio::main]
async fn main() -> Result<()> {
#[tracing::instrument]
fn main() -> Result<()> {
let args = argh::from_env::<Args>();
pretty_env_logger::formatted_builder()
.filter_module("theseus", log::LevelFilter::Info)
.target(pretty_env_logger::env_logger::Target::Stderr)
color_eyre::install()?;
let filter = EnvFilter::try_from_default_env()
.or_else(|_| EnvFilter::try_new("info"))?;
let format = fmt::layer()
.without_time()
.with_writer(std::io::stderr)
.with_target(false)
.compact();
tracing_subscriber::registry()
.with(format)
.with(filter)
.with(ErrorLayer::default())
.init();
args.dispatch()
.inspect_err(|_| error!("An error has occurred!\n"))
.await
tokio::runtime::Builder::new_multi_thread()
.enable_all()
.build()?
.block_on(
async move {
args.dispatch()
.inspect_err(|_| error!("An error has occurred!\n"))
.await
}
.with_current_subscriber(),
)
}

View File

@@ -1,17 +1,20 @@
use eyre::Result;
mod profile;
mod user;
#[derive(argh::FromArgs)]
#[derive(argh::FromArgs, Debug)]
#[argh(subcommand)]
pub enum SubCommand {
pub enum Subcommand {
Profile(profile::ProfileCommand),
User(user::UserCommand),
}
impl crate::Args {
pub async fn dispatch(&self) -> Result<()> {
match self.subcommand {
SubCommand::Profile(ref cmd) => cmd.dispatch(self).await,
}
dispatch!(self.subcommand, (self) => {
Subcommand::Profile,
Subcommand::User
})
}
}

View File

@@ -1,27 +1,26 @@
//! Profile management subcommand
use crate::util::{
confirm_async, prompt_async, select_async, table_path_display,
confirm_async, prompt_async, select_async, table, table_path_display,
};
use daedalus::modded::LoaderVersion;
use eyre::{ensure, Result};
use futures::prelude::*;
use paris::*;
use std::path::{Path, PathBuf};
use tabled::{Table, Tabled};
use tabled::Tabled;
use theseus::prelude::*;
use tokio::fs;
use tokio_stream::wrappers::ReadDirStream;
use uuid::Uuid;
#[derive(argh::FromArgs)]
#[derive(argh::FromArgs, Debug)]
#[argh(subcommand, name = "profile")]
/// profile management
/// manage Minecraft instances
pub struct ProfileCommand {
#[argh(subcommand)]
action: ProfileSubcommand,
}
#[derive(argh::FromArgs)]
#[derive(argh::FromArgs, Debug)]
#[argh(subcommand)]
pub enum ProfileSubcommand {
Add(ProfileAdd),
@@ -31,7 +30,7 @@ pub enum ProfileSubcommand {
Run(ProfileRun),
}
#[derive(argh::FromArgs)]
#[derive(argh::FromArgs, Debug)]
#[argh(subcommand, name = "add")]
/// add a new profile to Theseus
pub struct ProfileAdd {
@@ -71,7 +70,7 @@ impl ProfileAdd {
}
}
#[derive(argh::FromArgs)]
#[derive(argh::FromArgs, Debug)]
#[argh(subcommand, name = "init")]
/// create a new profile and manage it with Theseus
pub struct ProfileInit {
@@ -260,7 +259,7 @@ impl ProfileInit {
}
}
#[derive(argh::FromArgs)]
#[derive(argh::FromArgs, Debug)]
/// list all managed profiles
#[argh(subcommand, name = "list")]
pub struct ProfileList {}
@@ -311,16 +310,15 @@ impl ProfileList {
_args: &crate::Args,
_largs: &ProfileCommand,
) -> Result<()> {
let state = State::get().await?;
let profiles = state.profiles.read().await;
let profiles = profiles.0.iter().map(|(path, prof)| {
let profiles = profile::list().await?;
let rows = profiles.iter().map(|(path, prof)| {
prof.as_ref().map_or_else(
|| ProfileRow::from(path.as_path()),
ProfileRow::from,
)
});
let table = Table::new(profiles).with(tabled::Style::psql()).with(
let table = table(rows).with(
tabled::Modify::new(tabled::Column(1..=1))
.with(tabled::MaxWidth::wrapping(40)),
);
@@ -330,7 +328,7 @@ impl ProfileList {
}
}
#[derive(argh::FromArgs)]
#[derive(argh::FromArgs, Debug)]
/// unmanage a profile
#[argh(subcommand, name = "remove")]
pub struct ProfileRemove {
@@ -364,7 +362,7 @@ impl ProfileRemove {
}
}
#[derive(argh::FromArgs)]
#[derive(argh::FromArgs, Debug)]
/// run a profile
#[argh(subcommand, name = "run")]
pub struct ProfileRun {
@@ -372,18 +370,9 @@ pub struct ProfileRun {
/// the profile to run
profile: PathBuf,
// TODO: auth
#[argh(option, short = 't')]
/// the Minecraft token to use for player login. Should be replaced by auth when that is a thing.
token: String,
#[argh(option, short = 'n')]
/// the uername to use for running the game
name: String,
#[argh(option, short = 'i')]
/// the account id to use for running the game
id: Uuid,
#[argh(option)]
/// the user to authenticate with
user: Option<uuid::Uuid>,
}
impl ProfileRun {
@@ -400,11 +389,18 @@ impl ProfileRun {
"Profile not managed by Theseus (if it exists, try using `profile add` first!)",
);
let credentials = Credentials {
id: self.id.clone(),
username: self.name.clone(),
access_token: self.token.clone(),
};
let id = future::ready(self.user.ok_or(()))
.or_else(|_| async move {
let state = State::get().await?;
let settings = state.settings.read().await;
settings.default_user
.ok_or(eyre::eyre!(
"Could not find any users, please add one using the `user add` command."
))
})
.await?;
let credentials = auth::refresh(id, false).await?;
let mut proc = profile::run(&path, &credentials).await?;
profile::wait_for(&mut proc).await?;
@@ -415,14 +411,14 @@ impl ProfileRun {
}
impl ProfileCommand {
pub async fn dispatch(&self, args: &crate::Args) -> Result<()> {
match &self.action {
ProfileSubcommand::Add(ref cmd) => cmd.run(args, self).await,
ProfileSubcommand::Init(ref cmd) => cmd.run(args, self).await,
ProfileSubcommand::List(ref cmd) => cmd.run(args, self).await,
ProfileSubcommand::Remove(ref cmd) => cmd.run(args, self).await,
ProfileSubcommand::Run(ref cmd) => cmd.run(args, self).await,
}
pub async fn run(&self, args: &crate::Args) -> Result<()> {
dispatch!(&self.action, (args, self) => {
ProfileSubcommand::Add,
ProfileSubcommand::Init,
ProfileSubcommand::List,
ProfileSubcommand::Remove,
ProfileSubcommand::Run
})
}
}

View File

@@ -0,0 +1,178 @@
//! User management subcommand
use crate::util::{confirm_async, table};
use eyre::Result;
use paris::*;
use tabled::Tabled;
use theseus::prelude::*;
use tokio::sync::oneshot;
#[derive(argh::FromArgs, Debug)]
#[argh(subcommand, name = "user")]
/// manage Minecraft accounts
pub struct UserCommand {
#[argh(subcommand)]
action: UserSubcommand,
}
#[derive(argh::FromArgs, Debug)]
#[argh(subcommand)]
pub enum UserSubcommand {
Add(UserAdd),
List(UserList),
Remove(UserRemove),
SetDefault(UserDefault),
}
#[derive(argh::FromArgs, Debug)]
/// add a new user to Theseus
#[argh(subcommand, name = "add")]
pub struct UserAdd {
#[argh(option)]
/// the browser to authenticate using
browser: Option<webbrowser::Browser>,
}
impl UserAdd {
pub async fn run(
&self,
_args: &crate::Args,
_largs: &UserCommand,
) -> Result<()> {
info!("Adding new user account to Theseus");
info!("A browser window will now open, follow the login flow there.");
let (tx, rx) = oneshot::channel::<url::Url>();
let flow = tokio::spawn(auth::authenticate(tx));
let url = rx.await?;
match self.browser {
Some(browser) => webbrowser::open_browser(browser, url.as_str()),
None => webbrowser::open(url.as_str()),
}?;
let credentials = flow.await??;
State::sync().await?;
success!("Logged in user {}.", credentials.username);
Ok(())
}
}
#[derive(argh::FromArgs, Debug)]
/// list all known users
#[argh(subcommand, name = "list")]
pub struct UserList {}
#[derive(Tabled)]
struct UserRow<'a> {
username: &'a str,
id: uuid::Uuid,
default: bool,
}
impl<'a> UserRow<'a> {
pub fn from(
credentials: &'a Credentials,
default: Option<uuid::Uuid>,
) -> Self {
Self {
username: &credentials.username,
id: credentials.id,
default: Some(credentials.id) == default,
}
}
}
impl UserList {
pub async fn run(
&self,
_args: &crate::Args,
_largs: &UserCommand,
) -> Result<()> {
let state = State::get().await?;
let default = state.settings.read().await.default_user;
let users = auth::users().await?;
let rows = users.iter().map(|user| UserRow::from(user, default));
let table = table(rows);
println!("{table}");
Ok(())
}
}
#[derive(argh::FromArgs, Debug)]
/// remove a user
#[argh(subcommand, name = "remove")]
pub struct UserRemove {
/// the user to remove
#[argh(positional)]
user: uuid::Uuid,
}
impl UserRemove {
pub async fn run(
&self,
_args: &crate::Args,
_largs: &UserCommand,
) -> Result<()> {
info!("Removing user {}", self.user.as_hyphenated());
if confirm_async(String::from("Do you wish to continue"), true).await? {
if !auth::has_user(self.user).await? {
warn!("Profile was not managed by Theseus!");
} else {
auth::remove_user(self.user).await?;
State::sync().await?;
success!("User removed!");
}
} else {
error!("Aborted!");
}
Ok(())
}
}
#[derive(argh::FromArgs, Debug)]
/// set the default user
#[argh(subcommand, name = "set-default")]
pub struct UserDefault {
/// the user to set as default
#[argh(positional)]
user: uuid::Uuid,
}
impl UserDefault {
pub async fn run(
&self,
_args: &crate::Args,
_largs: &UserCommand,
) -> Result<()> {
info!("Setting user {} as default", self.user.as_hyphenated());
// TODO: settings API
let state: std::sync::Arc<State> = State::get().await?;
let mut settings = state.settings.write().await;
if settings.default_user == Some(self.user) {
warn!("User is already the default!");
} else {
settings.default_user = Some(self.user);
success!("User set as default!");
}
Ok(())
}
}
impl UserCommand {
pub async fn run(&self, args: &crate::Args) -> Result<()> {
dispatch!(&self.action, (args, self) => {
UserSubcommand::Add,
UserSubcommand::List,
UserSubcommand::Remove,
UserSubcommand::SetDefault
})
}
}

View File

@@ -1,6 +1,7 @@
use dialoguer::{Confirm, Input, Select};
use eyre::Result;
use std::{borrow::Cow, path::Path};
use tabled::{Table, Tabled};
// TODO: make primarily async to avoid copies
@@ -56,7 +57,11 @@ pub async fn confirm_async(prompt: String, default: bool) -> Result<bool> {
tokio::task::spawn_blocking(move || confirm(&prompt, default)).await?
}
// Table display helpers
// Table helpers
pub fn table<T: Tabled>(rows: impl IntoIterator<Item = T>) -> Table {
Table::new(rows).with(tabled::Style::psql())
}
pub fn table_path_display(path: &Path) -> String {
let mut res = path.display().to_string();
@@ -67,6 +72,20 @@ pub fn table_path_display(path: &Path) -> String {
res
}
// Dispatch macros
macro_rules! dispatch {
($on:expr, $args:tt => {$($option:path),+}) => {
match $on {
$($option (ref cmd) => dispatch!(@apply cmd => $args)),+
}
};
(@apply $cmd:expr => ($($args:expr),*)) => {{
use tracing_futures::WithSubscriber;
$cmd.run($($args),*).with_current_subscriber().await
}};
}
// Internal helpers
fn print_prompt(prompt: &str) {
println!(