Skip to content

Commit

Permalink
Merge pull request #818 from itowlson/refactor-login
Browse files Browse the repository at this point in the history
Break login into smaller functions
  • Loading branch information
itowlson authored Oct 16, 2022
2 parents 4dbd044 + 609cf0d commit bda5677
Showing 1 changed file with 203 additions and 116 deletions.
319 changes: 203 additions & 116 deletions src/commands/login.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
use std::io::stdin;
use std::io::{stdin, Write};
use std::path::PathBuf;
use std::time::Duration;

Expand Down Expand Up @@ -92,22 +92,31 @@ pub struct LoginCommand {
pub hippo_password: Option<String>,

/// Display login status
#[clap(name = "status", long = "status", takes_value = false)]
#[clap(
name = "status",
long = "status",
takes_value = false,
conflicts_with = "get-device-code",
conflicts_with = "check-device-code"
)]
pub status: bool,

// fetch a device code
#[clap(
name = "get-device-code",
long = "get-device-code",
takes_value = false
takes_value = false,
conflicts_with = "status",
conflicts_with = "check-device-code"
)]
pub get_device_code: bool,

// check a device code
#[clap(
name = "check-device-code",
long = "check-device-code",
takes_value = false
conflicts_with = "status",
conflicts_with = "get-device-code"
)]
pub check_device_code: Option<String>,

Expand All @@ -122,135 +131,186 @@ pub struct LoginCommand {
}

impl LoginCommand {
pub async fn run(self) -> Result<()> {
let root = dirs::config_dir()
.context("Cannot find configuration directory")?
.join("spin");
pub async fn run(&self) -> Result<()> {
match (self.status, self.get_device_code, &self.check_device_code) {
(true, false, None) => self.run_status().await,
(false, true, None) => self.run_get_device_code().await,
(false, false, Some(device_code)) => self.run_check_device_code(device_code).await,
(false, false, None) => self.run_interactive_login().await,
_ => Err(anyhow::anyhow!("Invalid combination of options")), // Should never happen
}
}

ensure(&root)?;
async fn run_status(&self) -> Result<()> {
let path = self.config_file_path()?;
let data = fs::read_to_string(&path)
.await
.context("Cannnot display login information")?;
println!("{}", data);
Ok(())
}

let path = root.join("config.json");
async fn run_get_device_code(&self) -> Result<()> {
let connection_config = self.anon_connection_config();
let device_code_info = create_device_code(&Client::new(connection_config)).await?;

if self.status {
let data = fs::read_to_string(path.clone())
.await
.context("Cannnot display login information")?;
println!("{}", data);
return Ok(());
}
println!("{}", serde_json::to_string_pretty(&device_code_info)?);

let login_connection: LoginConnection;
Ok(())
}

async fn run_check_device_code(&self, device_code: &str) -> Result<()> {
let connection_config = self.anon_connection_config();
let client = Client::new(connection_config);
let token_info = client.login(device_code.to_owned()).await?;

let auth_method = self.auth_method();
let token_readiness = if token_info.token.is_some() {
TokenReadiness::Ready(token_info)
} else {
TokenReadiness::Unready
};

let mut url = DEFAULT_CLOUD_URL.to_owned();
if let Some(u) = self.hippo_server_url {
url = u;
match token_readiness {
TokenReadiness::Ready(token_info) => {
println!("{}", serde_json::to_string_pretty(&token_info)?);
let login_connection = self.login_connection_for_token(token_info);
self.save_login_info(&login_connection)?;
}
TokenReadiness::Unready => {
let waiting = json!({ "status": "waiting" });
println!("{}", serde_json::to_string_pretty(&waiting)?);
}
}

// login and populate login_connection based on the auth type
if auth_method == AuthMethod::UsernameAndPassword {
let username = match self.hippo_username {
Some(username) => username,
None => {
print!("Hippo username: ");
let mut input = String::new();
stdin()
.read_line(&mut input)
.expect("unable to read user input");
input.trim().to_owned()
}
};
let password = match self.hippo_password {
Some(password) => password,
None => {
print!("Hippo pasword: ");
rpassword::read_password()
.expect("unable to read user input")
.trim()
.to_owned()
}
};
// log in with username/password
let token = match HippoClient::login(
&HippoClient::new(ConnectionInfo {
url: url.clone(),
danger_accept_invalid_certs: self.insecure,
api_key: None,
}),
username,
password,
)
.await
{
Ok(token_info) => token_info,
Err(err) => bail!(format_login_error(&err)?),
};
Ok(())
}

login_connection = LoginConnection {
url: url.clone(),
danger_accept_invalid_certs: self.insecure,
token: token.token.unwrap_or_default(),
expiration: token.expiration.unwrap_or_default(),
bindle_url: self.bindle_server_url,
bindle_username: self.bindle_username,
bindle_password: self.bindle_password,
};
} else {
// log in to the cloud API
let connection_config = ConnectionConfig {
url: url.clone(),
insecure: self.insecure,
token: Default::default(),
};
async fn run_interactive_login(&self) -> Result<()> {
let login_connection = match self.auth_method() {
AuthMethod::Github => self.run_interactive_gh_login().await?,
AuthMethod::UsernameAndPassword => self.run_interactive_basic_login().await?,
};
self.save_login_info(&login_connection)
}

if self.get_device_code {
println!(
"{}",
serde_json::to_string_pretty(
&create_device_code(&Client::new(connection_config)).await?
)?
);
return Ok(());
async fn run_interactive_gh_login(&self) -> Result<LoginConnection> {
// log in to the cloud API
let connection_config = self.anon_connection_config();
let token_info = github_token(connection_config).await?;

Ok(self.login_connection_for_token(token_info))
}

async fn run_interactive_basic_login(&self) -> Result<LoginConnection> {
let username = prompt_if_not_provided(&self.hippo_username, "Hippo username")?;
let password = match &self.hippo_password {
Some(password) => password.to_owned(),
None => {
print!("Hippo password: ");
std::io::stdout().flush()?;
rpassword::read_password()
.expect("unable to read user input")
.trim()
.to_owned()
}
};

let token: TokenInfo;
if let Some(device_code) = self.check_device_code {
let client = Client::new(connection_config);
match client.login(device_code).await {
Ok(token_info) => {
if token_info.token.is_some() {
println!("{}", serde_json::to_string_pretty(&token_info)?);
token = token_info;
} else {
println!(
"{}",
serde_json::to_string_pretty(&json!({ "status": "waiting" }))?
);
return Ok(());
}
}
Err(e) => {
return Err(e);
}
};
let bindle_url = prompt_if_not_provided(&self.bindle_server_url, "Bindle URL")?;

// If Bindle URL was provided and Bindle username and password were not, assume Bindle
// is unauthenticated. If Bindle URL was prompted for, or Bindle username or password
// is provided, ask the user.
let mut bindle_username = self.bindle_username.clone();
let mut bindle_password = self.bindle_password.clone();

let unauthenticated_bindle_server_provided = self.bindle_server_url.is_some()
&& self.bindle_username.is_none()
&& self.bindle_password.is_none();
if !unauthenticated_bindle_server_provided {
let bindle_username_text = prompt_if_not_provided(
&self.bindle_username,
"Bindle username (blank for unauthenticated)",
)?;
bindle_username = if bindle_username_text.is_empty() {
None
} else {
token = github_token(connection_config).await?;
}
Some(bindle_username_text)
};
bindle_password = match bindle_username {
None => None,
Some(_) => Some(prompt_if_not_provided(
&self.bindle_password,
"Bindle password",
)?),
};
}

login_connection = LoginConnection {
url,
// log in with username/password
let token = match HippoClient::login(
&HippoClient::new(ConnectionInfo {
url: self.url().to_owned(),
danger_accept_invalid_certs: self.insecure,
token: token.token.unwrap_or_default(),
expiration: token.expiration.unwrap_or_default(),
bindle_url: None,
bindle_username: None,
bindle_password: None,
};
api_key: None,
}),
username,
password,
)
.await
{
Ok(token_info) => token_info,
Err(err) => bail!(format_login_error(&err)?),
};

Ok(LoginConnection {
url: self.url().to_owned(),
danger_accept_invalid_certs: self.insecure,
token: token.token.unwrap_or_default(),
expiration: token.expiration.unwrap_or_default(),
bindle_url: Some(bindle_url),
bindle_username,
bindle_password,
})
}

fn login_connection_for_token(&self, token_info: TokenInfo) -> LoginConnection {
let login_connection = LoginConnection {
url: self.url().to_owned(),
danger_accept_invalid_certs: self.insecure,
token: token_info.token.unwrap_or_default(),
expiration: token_info.expiration.unwrap_or_default(),
bindle_url: None,
bindle_username: None,
bindle_password: None,
};
login_connection
}

fn config_file_path(&self) -> Result<PathBuf> {
let root = dirs::config_dir()
.context("Cannot find configuration directory")?
.join("spin");

ensure(&root)?;

let path = root.join("config.json");

Ok(path)
}

fn anon_connection_config(&self) -> ConnectionConfig {
ConnectionConfig {
url: self.url().to_owned(),
insecure: self.insecure,
token: Default::default(),
}
}

std::fs::write(path, serde_json::to_string_pretty(&login_connection)?)?;
Ok(())
fn url(&self) -> &str {
if let Some(u) = &self.hippo_server_url {
u
} else {
DEFAULT_CLOUD_URL
}
}

fn auth_method(&self) -> AuthMethod {
Expand All @@ -268,6 +328,27 @@ impl LoginCommand {
AuthMethod::Github
}
}

fn save_login_info(&self, login_connection: &LoginConnection) -> Result<(), anyhow::Error> {
let path = self.config_file_path()?;
std::fs::write(path, serde_json::to_string_pretty(login_connection)?)?;
Ok(())
}
}

fn prompt_if_not_provided(provided: &Option<String>, prompt_text: &str) -> Result<String> {
match provided {
Some(value) => Ok(value.to_owned()),
None => {
print!("{}: ", prompt_text);
std::io::stdout().flush()?;
let mut input = String::new();
stdin()
.read_line(&mut input)
.expect("unable to read user input");
Ok(input.trim().to_owned())
}
}
}

async fn github_token(
Expand Down Expand Up @@ -402,6 +483,7 @@ fn prompt_for_auth_method() -> AuthMethod {
loop {
// prompt the user for the authentication method
print!("What authentication method does this server support?\n\n1. Sign in with GitHub\n2. Sign in with a username and password\n\nEnter a number: ");
std::io::stdout().flush().unwrap();
let mut input = String::new();
stdin()
.read_line(&mut input)
Expand All @@ -420,3 +502,8 @@ fn prompt_for_auth_method() -> AuthMethod {
}
}
}

enum TokenReadiness {
Ready(TokenInfo),
Unready,
}

0 comments on commit bda5677

Please sign in to comment.