use std::io::Write as _;
use silicon_iam_client::{Credential, models};
use time::OffsetDateTime;
use crate::{
cli::{LoginArgs, SignupArgs, SiliconLoginArgs},
context::Context,
error::{CliError, Result},
output::{Format, Table, json},
store::Session,
};
pub fn session_from(tokens: &models::IamTokenResponse, carbon_id: &str) -> Session {
Session {
access_token: tokens.access_token.clone(),
refresh_token: tokens.refresh_token.clone(),
expires_at: OffsetDateTime::now_utc() + time::Duration::seconds(tokens.expires_in),
carbon_id: carbon_id.to_owned(),
}
}
pub async fn login(context: &Context, args: LoginArgs) -> Result<()> {
if args.email.is_none() && args.phone.is_none() && args.carbon_id.is_none() {
return Err(CliError::Usage(
"give one of --email, --phone or --carbon-id".to_owned(),
));
}
let client = context.anonymous();
let challenge = client
.auth()
.start_login(
&models::LoginChallengeCreate {
email: args.email.clone(),
phone_number: args.phone.clone(),
carbon_id: args.carbon_id.clone(),
},
&context.mutation(),
)
.await?;
let code = match args.code {
Some(code) => code,
None => match challenge.local_otp.clone() {
Some(code) => code,
None => prompt("Verification code: ")?,
},
};
let tokens = client
.auth()
.verify_login(challenge.session_id, &code, &context.mutation())
.await?;
let signed_in = client
.with_credential(Credential::bearer(tokens.access_token.clone()))
.carbons()
.me()
.await?;
context.remember(session_from(&tokens, &signed_in.carbon_id))?;
if let Some(app_id) = args.app_id.as_deref() {
return report_short_lived_token(context, &tokens.access_token, app_id).await;
}
match context.format {
Format::Json => json(&signed_in),
Format::Text => {
println!(
"Signed in as {} on profile {}.",
signed_in.carbon_id, context.profile_name
);
Ok(())
}
}
}
pub async fn silicon_login(context: &Context, args: SiliconLoginArgs) -> Result<()> {
let sid = match args.sid {
Some(value) => value,
None => prompt("Silicon ID: ")?,
};
let stk = match args.stk {
Some(value) => value,
None => prompt("Silicon token: ")?,
};
let client = context.anonymous();
let tokens = client
.auth()
.authenticate_silicon(
&models::SiliconAuthenticationRequest {
silicon_id: sid.clone(),
silicon_token: stk,
},
&context.mutation(),
)
.await?;
if let Some(app_id) = args.app_id.as_deref() {
return report_short_lived_token(context, &tokens.access_token, app_id).await;
}
match context.format {
Format::Json => json(&tokens),
Format::Text => {
println!("Signed in as {sid}.");
println!("Access token: {}", tokens.access_token);
println!("Refresh token: {}", tokens.refresh_token);
Ok(())
}
}
}
async fn report_short_lived_token(
context: &Context,
access_token: &str,
app_id: &str,
) -> Result<()> {
let issued = context
.anonymous()
.with_credential(Credential::bearer(access_token.to_owned()))
.auth()
.short_lived_token(app_id, &context.mutation())
.await?;
match context.format {
Format::Json => json(&issued),
Format::Text => {
println!("Short-lived token for {app_id}: {}", issued.slt);
println!(
"It is good for {} seconds and one exchange.",
issued.expires_in
);
Ok(())
}
}
}
pub fn logout(context: &Context) -> Result<()> {
if context.forget()? {
println!("Signed out of profile {}.", context.profile_name);
} else {
println!("Profile {} was not signed in.", context.profile_name);
}
Ok(())
}
pub async fn whoami(context: &Context) -> Result<()> {
let me = context.authenticated().await?.carbons().me().await?;
match context.format {
Format::Json => json(&me),
Format::Text => {
let mut table = Table::new(["field", "value"]);
table.row(["carbon_id", &me.carbon_id]);
table.row(["display_name", &me.display_name]);
table.row(["email", &me.email]);
table.row(["phone", &me.phone_number]);
table.row(["timezone", &me.timezone]);
table.row(["profile", &context.profile_name]);
table.row(["service", context.anonymous().base_url().as_str()]);
if let Some(environment_id) = context.testing_environment_id() {
table.row(["test_environment", &environment_id.to_string()]);
}
table.print();
Ok(())
}
}
}
pub async fn signup(context: &Context, args: SignupArgs) -> Result<()> {
let client = context.anonymous();
let session = client.signup().start(&context.mutation()).await?.session_id;
let dispatched = client
.signup()
.send_email_code(session, &args.email, &context.mutation())
.await?;
if dispatched.already_exists {
return Err(CliError::Usage(format!(
"{} already belongs to a Carbon; sign in instead",
args.email
)));
}
let code = collect_code(dispatched.local_otp, "Email verification code: ")?;
client
.signup()
.verify_email(session, &code, &context.mutation())
.await?;
let dispatched = client
.signup()
.send_phone_code(session, &args.phone, &context.mutation())
.await?;
if dispatched.already_exists {
return Err(CliError::Usage(format!(
"{} already belongs to a Carbon; sign in instead",
args.phone
)));
}
let code = collect_code(dispatched.local_otp, "Phone verification code: ")?;
client
.signup()
.verify_phone(session, &code, &context.mutation())
.await?;
let created = client
.signup()
.complete(
session,
&models::CarbonSignupComplete {
carbon_id: args.carbon_id.clone(),
display_name: args.display_name.unwrap_or_else(|| args.carbon_id.clone()),
timezone: args.timezone.map(serde_json::Value::String),
description: None,
profile_photo: None,
},
&context.mutation(),
)
.await?;
match context.format {
Format::Json => json(&created),
Format::Text => {
println!(
"Created {}. Run `iam login --carbon-id {}` to sign in.",
created.carbon_id, created.carbon_id
);
Ok(())
}
}
}
fn collect_code(echoed: Option<String>, prompt_text: &str) -> Result<String> {
match echoed {
Some(code) => Ok(code),
None => prompt(prompt_text),
}
}
pub(crate) fn prompt(label: &str) -> Result<String> {
print!("{label}");
std::io::stdout().flush()?;
let mut line = String::new();
std::io::stdin().read_line(&mut line)?;
Ok(line.trim().to_owned())
}