archival 0.14.0

The simplest CMS in existence
Documentation
use super::BinaryCommand;
use crate::{
    binary::{
        command::{add_args, CommandConfig},
        ArchivalConfig, ExitStatus,
    },
    constants::{API_URL, AUTH_URL, CLI_TOKEN_PUBLIC_KEY},
};
use base64::{engine::general_purpose::STANDARD, Engine as _};
use clap::ArgMatches;
use indicatif::{ProgressBar, ProgressStyle};
use nanoid::nanoid;
use reqwest::StatusCode;
use rsa::{pkcs8::DecodePublicKey, sha2::Sha256, Oaep, RsaPublicKey};
use std::{
    fs,
    sync::{atomic::AtomicBool, Arc},
    thread,
    time::Duration,
};

pub struct Command {}
impl BinaryCommand for Command {
    fn name(&self) -> &str {
        "login"
    }
    fn cli(&self, cmd: clap::Command) -> clap::Command {
        add_args(
            cmd.about("Log in to archival.dev and store credentials locally in ~/.archivalrc"),
            CommandConfig::default(),
        )
    }
    fn handler(
        &self,
        _args: &ArgMatches,
        _quit: Arc<AtomicBool>,
    ) -> Result<crate::binary::ExitStatus, Box<dyn std::error::Error>> {
        let config_file_path = ArchivalConfig::location();
        let secret_client_id = nanoid!(21);
        let public_key = RsaPublicKey::from_public_key_pem(CLI_TOKEN_PUBLIC_KEY).unwrap();
        let mut rng = rand::thread_rng();
        let padding = Oaep::new::<Sha256>();
        let enc_data = public_key.encrypt(&mut rng, padding, secret_client_id.as_bytes())?;
        let b64_encoded = STANDARD.encode(enc_data);
        let auth_url = format!(
            "{}?cli-code={}",
            AUTH_URL,
            urlencoding::encode(&b64_encoded)
        );
        let token_url = format!("{}/cli-token", API_URL);
        println!("To log in, open this URL in your browser\n{}", auth_url);
        let bar = ProgressBar::new_spinner();
        bar.set_style(ProgressStyle::with_template("{msg} {spinner}").unwrap());
        bar.set_message("waiting for login to complete");
        bar.enable_steady_tick(Duration::from_millis(100));
        let handle = thread::spawn(move || {
            let client = reqwest::blocking::Client::new();
            let mut access_token = None;
            loop {
                if let Ok(response) = client
                    .post(token_url.to_owned())
                    .body(secret_client_id.to_owned())
                    .send()
                {
                    let status = response.status();
                    if let Ok(t) = response.text() {
                        if status.is_success() {
                            access_token = Some(t);
                        } else if status != StatusCode::NOT_FOUND {
                            bar.println(format!("server returned an error: {}", t));
                        }
                    }
                }
                if access_token.is_some() {
                    break;
                }
                thread::sleep(Duration::from_millis(1000));
            }
            let config = if let Ok(Some(mut existing)) = ArchivalConfig::from_fs() {
                existing.access_token = access_token;
                existing
            } else {
                ArchivalConfig { access_token }
            };
            let config_str = toml::to_string(&config).unwrap();
            fs::write(config_file_path, config_str.as_bytes()).expect("failed writing config");
            bar.finish();
        });
        handle.join().unwrap();
        println!("successfully logged in.");
        Ok(ExitStatus::Ok)
    }
}