portablemc-cli 5.0.3

Cross platform command line utility for launching Minecraft quickly and reliably with included support for Mojang versions and popular mod loaders.
//! Implementation of the 'auth' command.

use std::process::ExitCode;

use portablemc::msa::{Account, Auth, AuthError};
use uuid::Uuid;

use crate::output::LogLevel;
use crate::parse::{AuthArgs, AuthCmd};

use super::{Cli, log_msa_auth_error, log_msa_database_error};


pub fn auth(cli: &mut Cli, args: &AuthArgs) -> ExitCode {
    match &args.cmd {
        AuthCmd::Login(login_args) => 
            auth_login(cli, login_args.no_browser),
        AuthCmd::List(_list_args) => 
            auth_list(cli),
        AuthCmd::Refresh(refresh_args) => 
            auth_account_action(cli, &refresh_args.account, AccountAction::Refresh),
        AuthCmd::Forget(forget_args) => 
            auth_account_action(cli, &forget_args.account, AccountAction::Forget),
    }
}

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum AccountAction {
    Forget,
    Refresh,
}

fn auth_account_action(cli: &mut Cli, name: &str, action: AccountAction) -> ExitCode {

    let res = 
    if let Ok(uuid) = Uuid::parse_str(&name) {
        match action {
            AccountAction::Forget => cli.msa_db.remove_from_uuid(uuid),
            AccountAction::Refresh => cli.msa_db.load_from_uuid(uuid),
        }
    } else {
        match action {
            AccountAction::Forget => cli.msa_db.remove_from_username(&name),
            AccountAction::Refresh => cli.msa_db.load_from_username(&name),
        }
    };

    let mut account = match res {
        Ok(Some(account)) => account,
        Ok(None) => {

            cli.out.log("auth_account_not_found")
                .arg(name)
                .warning(format_args!("No account found for: {name}"));

            return ExitCode::SUCCESS;

        }
        Err(error) => {
            log_msa_database_error(cli, &error);
            return ExitCode::FAILURE;
        }
    };

    match action {
        AccountAction::Forget => {

            cli.out.log("auth_account_forgot")
                .arg(account.uuid())
                .arg(account.username())
                .success(format_args!("Forgot account {} ({})", account.username(), account.uuid()));

            ExitCode::SUCCESS
            
        }
        AccountAction::Refresh => {

            if refresh_account(cli, &mut account, false) {
                ExitCode::SUCCESS
            } else {
                ExitCode::FAILURE
            }

        }
    }

}

pub(crate) fn refresh_account(cli: &mut Cli, account: &mut Account, silent: bool) -> bool {

    cli.out.log("auth_account_refresh_profile")
        .arg(account.uuid())
        .arg(account.username())
        .line(if silent { LogLevel::Info } else { LogLevel::Pending }, 
            format_args!("Refreshing account profile for {}", account.uuid()));

    let mut refreshed_token = false;

    match account.request_profile() {
        Ok(()) => {}
        Err(AuthError::OutdatedToken) => {
            
            cli.out.log("auth_account_refresh_token")
                .arg(account.uuid())
                .arg(account.username())
                .pending(format_args!("Refreshing account token for {}", account.uuid()));

            match account.request_refresh() {
                Ok(()) => {
                    refreshed_token = true;
                }
                Err(error) => {
                    log_msa_auth_error(cli, &error);
                    return false;
                }
            }

        }
        Err(error) => {
            log_msa_auth_error(cli, &error);
            return false;
        }
    };

    cli.out.log("auth_account_refreshed")
        .arg(account.uuid())
        .arg(account.username())
        .line(if silent && !refreshed_token { LogLevel::Info } else { LogLevel::Success }, 
            format_args!("Refreshed account as {} ({})", account.username(), account.uuid()));
    
    // Once the account is refreshed, store it!
    match cli.msa_db.store(account.clone()) {
        Ok(()) => true,
        Err(error) => {
            log_msa_database_error(cli, &error);
            false
        }
    }

}

fn auth_list(cli: &mut Cli) -> ExitCode {

    let iter = match cli.msa_db.load_iter() {
        Ok(iter) => iter,
        Err(error) => {
            log_msa_database_error(cli, &error);
            return ExitCode::FAILURE;
        }
    };

    // Now we construct the table...
    let mut table = cli.out.table(2);

    {
        let mut row = table.row();
        row.cell("username").format("Username");
        row.cell("uuid").format("UUID");
    }
    
    table.sep();

    for account in iter {
        let mut row = table.row();
        row.cell(account.username());
        row.cell(account.uuid());
    }

    ExitCode::SUCCESS

}

fn auth_login(cli: &mut Cli, no_browser: bool) -> ExitCode {

    let auth = Auth::new(&cli.msa_azure_app_id);

    cli.out.log("auth_request_device_code")
        .pending("Requesting authentication device code...");

    let code_flow = match auth.request_device_code() {
        Ok(ret) => ret,
        Err(error) => {
            log_msa_auth_error(cli, &error);
            return ExitCode::FAILURE;
        }
    };

    cli.out.log("auth_device_code")
        .arg(code_flow.verification_uri())
        .arg(code_flow.user_code())
        .success(code_flow.message());

    if cli.out.is_human() && !no_browser {

        cli.out.log("auth_web_browser_open_prompt")
            .pending("Read the message above, and then press enter to open your web browser")
            .prompt(None);

        if webbrowser::open(code_flow.verification_uri()).is_ok() {
            cli.out.log("auth_web_browser_opened")
                .success("Your web browser has been opened");
        }

    }

    cli.out.log("auth_wait")
        .pending("Waiting for authentication to complete...");

    let account = match code_flow.wait() {
        Ok(account) => account,
        Err(error) => {
            log_msa_auth_error(cli, &error);
            return ExitCode::FAILURE;
        }
    };

    cli.out.log("auth_account_authenticated")
        .arg(account.uuid())
        .arg(account.username())
        .success(format_args!("Authenticated account as {} ({})", account.username(), account.uuid()));

    match cli.msa_db.store(account) {
        Ok(()) => ExitCode::SUCCESS,
        Err(error) => {
            log_msa_database_error(cli, &error);
            ExitCode::FAILURE
        }
    }
    
}