libwally/commands/
login.rs1use std::path::{Path, PathBuf};
2use std::thread::sleep;
3use std::time::Duration;
4
5use anyhow::format_err;
6use opener;
7use reqwest::blocking::Client;
8use reqwest::Url;
9use serde::Deserialize;
10use structopt::StructOpt;
11
12use crate::{
13 auth::AuthStore,
14 manifest::Manifest,
15 package_index::{PackageIndex, PackageIndexConfig},
16};
17
18#[derive(Debug, StructOpt)]
20pub struct LoginSubcommand {
21 #[structopt(long = "project-path", default_value = ".")]
23 pub project_path: PathBuf,
24 #[structopt(long = "token")]
26 pub token: Option<String>,
27 #[structopt(long = "api")]
29 pub api: Option<String>,
30}
31
32#[derive(Debug, Deserialize)]
33struct DeviceCodeResponse {
34 device_code: String,
35 user_code: String,
36 verification_uri: String,
37 expires_in: u64,
38 interval: u64,
39}
40
41#[derive(Deserialize)]
42struct DeviceCodeAuth {
43 access_token: String,
44 token_type: String,
45 scope: String,
46}
47
48#[derive(Deserialize)]
49#[serde(untagged)]
50enum AuthResponse {
51 Ok(DeviceCodeAuth),
52 Err { error: String },
53}
54
55fn wait_for_github_auth(
56 device_code_response: DeviceCodeResponse,
57 github_oauth_id: &str,
58) -> anyhow::Result<DeviceCodeAuth> {
59 sleep(Duration::from_secs(device_code_response.interval));
60
61 let client = Client::new();
62 let response = client
63 .post("https://github.com/login/oauth/access_token")
64 .header("accept", "application/json")
65 .json(&serde_json::json!({
66 "client_id": github_oauth_id,
67 "device_code": device_code_response.device_code,
68 "grant_type": "urn:ietf:params:oauth:grant-type:device_code",
69 }))
70 .send()?
71 .json::<AuthResponse>()?;
72
73 match response {
74 AuthResponse::Ok(auth) => Ok(auth),
75 AuthResponse::Err { error } => match error.as_ref() {
76 "authorization_pending" => wait_for_github_auth(device_code_response, github_oauth_id),
77 _ => Err(format_err!("Oauth request error: {}", error)),
78 },
79 }
80}
81
82fn prompt_api_key(api: url::Url) -> anyhow::Result<()> {
83 println!("Enter an API token for {}", api);
84 println!();
85 let token = rpassword::prompt_password_stdout("Enter token: ")?;
86
87 AuthStore::set_token(api.as_str(), Some(&token))
88}
89
90fn prompt_github_auth(api: url::Url, github_oauth_id: &str) -> anyhow::Result<()> {
91 let client = Client::new();
92 let device_code_response = client
93 .post("https://github.com/login/device/code")
94 .header("accept", "application/json")
95 .json(&serde_json::json!({
96 "client_id": github_oauth_id,
97 "scope": "read:user",
98 }))
99 .send()?
100 .json::<DeviceCodeResponse>()?;
101
102 println!();
103 println!("Go to {}", device_code_response.verification_uri);
104 println!("And enter the code: {}", device_code_response.user_code);
105 println!();
106
107 opener::open(&device_code_response.verification_uri).ok();
108
109 println!("Awaiting authorization...");
110 let auth = wait_for_github_auth(device_code_response, github_oauth_id)?;
111
112 println!("Authorization successful!");
113 AuthStore::set_token(api.as_str(), Some(&auth.access_token))
114}
115
116fn fetch_package_index_config(project_path: &Path) -> anyhow::Result<PackageIndexConfig> {
117 let manifest = Manifest::load(project_path)?;
118 let registry = Url::parse(&manifest.package.registry)?;
119 let package_index = PackageIndex::new(®istry, None)?;
120 package_index.config()
121}
122
123impl LoginSubcommand {
124 pub fn run(self) -> anyhow::Result<()> {
125 match (self.token, self.api) {
126 (Some(token), Some(api)) => AuthStore::set_token(&api, Some(&token)),
127 (Some(token), None) => {
128 let config = fetch_package_index_config(&self.project_path)?;
129
130 AuthStore::set_token(config.api.as_str(), Some(&token))
131 }
132 (None, _) => {
133 let config = fetch_package_index_config(&self.project_path)?;
134
135 match config.github_oauth_id {
136 None => prompt_api_key(config.api),
137 Some(github_oauth_id) => prompt_github_auth(config.api, &github_oauth_id),
138 }
139 }
140 }
141 }
142}