envoy-cli 0.1.6-hotfix

A Git-like CLI for managing encrypted environment files
use anyhow::{Ok, Result, bail};
use console::style;
use reqwest::Client;
use serde::Deserialize;
use tokio::time::{Duration, Instant, sleep};

use crate::utils::{
    config::{auth_server_url, load_token, logout, save_token},
    ui::{create_spinner, print_header, print_info, print_kv, print_success},
};

#[derive(Deserialize)]
struct DeviceCodeResponse {
    device_code: String,
    user_code: String,
    verification_uri: String,
    interval: u64,
    expires_in: u64,
}

#[derive(Deserialize)]
#[serde(untagged)]
enum TokenResponse {
    Success {
        #[serde(rename = "apiToken")]
        api_token: String,
    },
    Pending {
        error: String,
    },
}

pub async fn login() -> Result<()> {
    if load_token().is_ok() {
        print_info("Already logged in");
        return Ok(());
    }

    let client = Client::new();

    let device: DeviceCodeResponse = client
        .post(format!("{}/auth/github/device", auth_server_url()))
        .send()
        .await?
        .json::<DeviceCodeResponse>()
        .await?;

    print_header("GitHub Authentication");
    print_kv("Open:", &device.verification_uri);
    println!(
        "  {} {}",
        style("Code:").dim(),
        style(&device.user_code).yellow().bold()
    );
    println!();

    let spinner = create_spinner("Waiting for authorization...");

    let deadline = Instant::now() + Duration::from_secs(device.expires_in);

    loop {
        if Instant::now() >= deadline {
            spinner.finish_and_clear();
            bail!("Authorization timed out");
        }

        sleep(Duration::from_secs(device.interval)).await;

        let response_text = client
            .post(format!("{}/auth/github/token", auth_server_url()))
            .json(&serde_json::json!({
                "device_code": device.device_code
            }))
            .send()
            .await?
            .text()
            .await?;

        let res: TokenResponse = serde_json::from_str(&response_text)?;
        match res {
            TokenResponse::Pending { error } => {
                if error == "slow_down" {
                    sleep(Duration::from_secs(device.interval + 5)).await;
                }
                continue;
            }

            TokenResponse::Success { api_token } => {
                spinner.finish_and_clear();
                print_success("Authentication successful!");
                save_token(&api_token)?;
                return Ok(());
            }
        }
    }
}

pub fn logout_command() -> Result<()> {
    logout()?;
    Ok(())
}