zero-cli 1.0.1

A command line tool for Zero Secrets Manager
pub mod graphql;
use crate::common::{
    authorization_headers::authorization_headers,
    env_var::env_var,
    execute_graphql_request::execute_graphql_request,
    graphql::generate_secret_sharing_url::{generate_secret_sharing_url, GenerateSecretSharingUrl},
    keyring::keyring,
    print_formatted_error::print_formatted_error,
    query_full_id::{query_full_id, QueryType},
};
use crate::projects::share::graphql::project_secrets_ids::{
    project_secrets_ids, ProjectSecretsIds,
};
use chrono::{Duration, Utc};
use clap::Args;
use dialoguer::{theme::ColorfulTheme, MultiSelect, Password, Select};
use graphql_client::GraphQLQuery;
use reqwest::Client;
use termimad::crossterm::style::Stylize;

#[derive(Args, Debug)]
pub struct ProjectsShareArgs {
    #[clap(
        short,
        long,
        help = "Project ID (First 4 characters or more are allowed)"
    )]
    id: String,
    #[clap(
        short,
        long,
        help = "Access token, if not specified, the token will be taken from the keychain"
    )]
    access_token: Option<String>,
}

pub fn share(args: &ProjectsShareArgs) {
    let access_token = match &args.access_token {
        Some(token) => token.clone(),
        None => keyring::get("access_token"),
    };

    let project_id = query_full_id(QueryType::Project, args.id.clone(), &access_token);
    let user_secrets_error_message = format!("No secrets found for project '{}'.", &project_id);

    let project = match execute_graphql_request::<
        project_secrets_ids::Variables,
        project_secrets_ids::ResponseData,
    >(
        authorization_headers(&access_token),
        ProjectSecretsIds::build_query,
        &Client::new(),
        &user_secrets_error_message,
        project_secrets_ids::Variables {
            project_id: project_id,
        },
    )
    .token_by_pk
    {
        Some(project) => project,

        None => {
            print_formatted_error(&user_secrets_error_message);
            std::process::exit(1);
        }
    };

    let user_secrets = match project.user_secret.len() {
        1.. => &project.user_secret,

        _ => {
            print_formatted_error("No secrets found.");
            std::process::exit(1);
        }
    };

    let passphrase = match Password::with_theme(&ColorfulTheme::default())
        .with_prompt("Type a passphrase of at least 1 character:")
        .validate_with(|input: &String| -> Result<(), &str> {
            if input.trim().chars().count() > 0 {
                Ok(())
            } else {
                print_formatted_error("Passphrase must be at least 1 character long.");
                std::process::exit(1);
            }
        })
        .interact()
    {
        Ok(passphrase) => passphrase,
        Err(_) => {
            print_formatted_error("Failed to read passphrase.");
            std::process::exit(1);
        }
    };

    let options = ["5 min", "30 min", "1 hour", "1 day"];

    let expires_at_selections = match Select::with_theme(&ColorfulTheme::default())
        .with_prompt("Expires in:")
        .items(
            &options
                .iter()
                .map(|item| item.to_string())
                .collect::<Vec<String>>(),
        )
        .max_length(match env_var("ITEMS_PER_PAGE").parse::<usize>() {
            Ok(max_length) => max_length,
            Err(_) => {
                print_formatted_error("Failed to parse ITEMS_PER_PAGE.");
                std::process::exit(1);
            }
        })
        .interact()
    {
        Ok(expires_at_selections) => expires_at_selections,
        Err(_) => {
            print_formatted_error("Failed to read expires at selections.");
            std::process::exit(1);
        }
    };

    let expires_at_minutes = match options[expires_at_selections] {
        "5 min" => 5,
        "30 min" => 30,
        "1 hour" => 60,
        "1 day" => 1440,

        _ => {
            print_formatted_error("Invalid time selection.");
            std::process::exit(1);
        }
    };

    let items_per_page: usize = match env_var("ITEMS_PER_PAGE").parse() {
        Ok(items_per_page) => items_per_page,
        Err(_) => {
            print_formatted_error("Fail parsing the number of items per page.");
            std::process::exit(1);
        }
    };

    let secrets_selections = match MultiSelect::with_theme(&ColorfulTheme::default())
        .with_prompt("Pick secrets:")
        .items(
            &user_secrets
                .iter()
                .map(|user_secret| user_secret.name.to_string())
                .collect::<Vec<String>>(),
        )
        .max_length(items_per_page)
        .interact()
    {
        Ok(secrets_selections) => secrets_selections,
        Err(_) => {
            print_formatted_error("Failed to read secrets selections.");
            std::process::exit(1);
        }
    };

    if secrets_selections.is_empty() {
        print_formatted_error("You must select at least one secret.");
        std::process::exit(1);
    }

    let user_secret_fields_ids_vec: Vec<String> = user_secrets
        .iter()
        .filter_map(|user_secret| {
            secrets_selections
                .iter()
                .find(|&selection| user_secrets[*selection].name == user_secret.name)
                .map(|_| user_secret.fields.iter().map(|field| field.id.to_string()))
        })
        .flatten()
        .collect();

    let secrets_sharing_url = execute_graphql_request::<
        generate_secret_sharing_url::Variables,
        generate_secret_sharing_url::ResponseData,
    >(
        authorization_headers(&access_token),
        GenerateSecretSharingUrl::build_query,
        &Client::new(),
        &format!(
            "Failed to generate secret sharing url for project '{}'.",
            &project_id
        ),
        generate_secret_sharing_url::Variables::new(
            Utc::now() + Duration::minutes(expires_at_minutes),
            Some(passphrase),
            user_secret_fields_ids_vec,
            project_id,
        ),
    )
    .generate_secret_sharing_url
    .url;

    println!(
        "Your link with secrets for the '{}' project",
        &project_id.to_string()[..4].dark_cyan()
    );

    println!("{}", &secrets_sharing_url);
}