Skip to main content

dvm_run/
login.rs

1//! Logging in from a terminal (`dvm-run login`): dvm.run makes an API key
2//! for the user who approves the login in their browser, and the terminal
3//! saves it where the SDK finds it, so no one copies a key. Over SSH, the
4//! user opens the link it prints wherever they like. The Python and JS
5//! SDKs' `dvm-run login` is the same.
6
7use std::process::{Command, Stdio};
8
9use dvm_api::{CollectCliLogin, CreatedApiKey, Me, NewCliLogin, StartedCliLogin};
10use reqwest::Method;
11
12use crate::client::{Client, api_url_of};
13use crate::error::Result;
14
15/// A login's key, which [`save_key`](crate::save_key) keeps where the SDK
16/// finds it.
17#[derive(Clone, Debug)]
18pub struct LoggedIn {
19    pub api_url: String,
20    /// The user it acts as.
21    pub login: String,
22    pub key_name: String,
23    pub key: String,
24}
25
26/// Logs this machine in to dvm.run at `api_url`, else `DVM_API_URL`, else
27/// `https://sandbox.dvm.run`, once its user approves the login in their
28/// browser: the key it was given. `say` is told what to do; with
29/// `open_browser`, the page that approves it opens here.
30pub async fn login(
31    api_url: Option<&str>,
32    open_browser: bool,
33    mut say: impl FnMut(&str),
34) -> Result<LoggedIn> {
35    let url = api_url_of(api_url);
36    let terminal = Client::with_key(url.clone(), None);
37    let machine = NewCliLogin {
38        machine: gethostname::gethostname().to_string_lossy().into_owned(),
39    };
40    let started: StartedCliLogin = terminal
41        .json(Method::POST, "/cli-logins", &[], Some(&machine))
42        .await?;
43    say(&format!(
44        "Approve this login in your browser: {}",
45        started.approve_url
46    ));
47    say(&format!("The page shows the code {}.", started.code));
48    if open_browser {
49        open_in_browser(&started.approve_url);
50    }
51    let collect = CollectCliLogin {
52        collect_token: started.collect_token,
53    };
54    let created = loop {
55        let answer: Option<CreatedApiKey> = terminal
56            .json(Method::POST, "/cli-logins/key", &[], Some(&collect))
57            .await?;
58        if let Some(created) = answer {
59            break created;
60        }
61    };
62    let me: Me = Client::new(url.clone(), created.secret.clone())
63        .json(Method::GET, "/me", &[], None::<&()>)
64        .await?;
65    Ok(LoggedIn {
66        api_url: url,
67        login: me.login,
68        key_name: created.key.name,
69        key: created.secret,
70    })
71}
72
73/// With no browser here, the printed link is the way, so a failure to open
74/// one is no error.
75fn open_in_browser(url: &str) {
76    let opener = if cfg!(target_os = "macos") {
77        Command::new("open").arg(url).spawn()
78    } else if cfg!(windows) {
79        Command::new("cmd").args(["/c", "start", "", url]).spawn()
80    } else {
81        Command::new("xdg-open")
82            .arg(url)
83            .stdin(Stdio::null())
84            .stdout(Stdio::null())
85            .stderr(Stdio::null())
86            .spawn()
87    };
88    drop(opener);
89}