smbpndk-cli 0.3.7

Command line tool for creating and managing SmbPndk resources.
Documentation
use super::SignupMethod;
use crate::account::{
    lib::authorize_github,
    model::{Data, Status},
};
use anyhow::{anyhow, Result};
use console::{style, Term};
use dialoguer::{theme::ColorfulTheme, Input, Password, Select};
use log::debug;
use reqwest::{Client, StatusCode};
use serde::{Deserialize, Serialize};
use smbpndk_model::CommandResult;

use smbpndk_networking::{smb_base_url_builder, smb_token_file_path};
use smbpndk_utils::email_validation;
use spinners::Spinner;
use std::fmt::{Display, Formatter};

pub struct SignupArgs {
    pub email: String,
    pub password: Option<String>,
    pub password_confirmation: Option<String>,
    pub authorizations_attributes: Vec<Provider>,
}

#[derive(Debug, Serialize)]
pub struct Provider {
    pub uid: String,
    pub provider: i8,
}

#[derive(Debug, Serialize)]
pub struct SignupGithubParams {
    pub user: SignupUserGithub,
}

#[derive(Debug, Serialize)]
pub struct SignupEmailParams {
    pub user: SignupUserEmail,
}

#[derive(Debug, Serialize)]
pub struct SignupUserGithub {
    pub email: String,
    pub authorizations_attributes: Vec<Provider>,
}

#[derive(Debug, Serialize)]
pub struct SignupUserEmail {
    pub email: String,
    pub password: String,
}

#[derive(Debug, Serialize, Deserialize)]
struct SignupResult {
    status: Status,
    data: Option<Data>,
}

pub async fn process_signup() -> Result<CommandResult> {
    // Check if token file exists
    if smb_token_file_path().is_some() {
        return Ok(CommandResult {
            spinner: Spinner::new(
                spinners::Spinners::SimpleDotsScrolling,
                style("Loading...").green().bold().to_string(),
            ),
            symbol: "".to_owned(),
            msg: "You are already logged in. Please logout first.".to_owned(),
        });
    }

    let signup_methods = vec![SignupMethod::Email, SignupMethod::GitHub];
    let selection = Select::with_theme(&ColorfulTheme::default())
        .items(&signup_methods)
        .default(0)
        .interact_on_opt(&Term::stderr())
        .map(|i| signup_methods[i.unwrap()])
        .unwrap();

    match selection {
        SignupMethod::Email => signup_with_email(None).await,
        SignupMethod::GitHub => signup_with_github().await,
    }
}

pub async fn signup_with_email(email: Option<String>) -> Result<CommandResult> {
    let email = if let Some(email) = email {
        email
    } else {
        Input::<String>::with_theme(&ColorfulTheme::default())
            .with_prompt("Username")
            .validate_with(|email: &String| email_validation(email))
            .interact()
            .unwrap()
    };

    let password = Password::with_theme(&ColorfulTheme::default())
        .with_prompt("Password")
        .validate_with(|input: &String| -> Result<(), &str> {
            if input.len() >= 6 {
                Ok(())
            } else {
                Err("Password must be at least 6 characters")
            }
        })
        .with_confirmation("Confirm password", "Passwords do not match")
        .interact()
        .unwrap();

    let spinner = Spinner::new(
        spinners::Spinners::BouncingBall,
        style("Signing up...").green().bold().to_string(),
    );

    let params = SignupEmailParams {
        user: SignupUserEmail { email, password },
    };

    match do_signup(&params).await {
        Ok(_) => Ok(CommandResult {
            spinner,
            symbol: style("".to_string()).for_stderr().green().to_string(),
            msg: "You are signed up! Check your email to confirm your account.".to_owned(),
        }),
        Err(e) => Ok(CommandResult {
            spinner,
            symbol: style("".to_string()).for_stderr().red().to_string(),
            msg: format!("{e}"),
        }),
    }
}

async fn signup_with_github() -> Result<CommandResult> {
    match authorize_github().await {
        Ok(code) => {
            debug!("Code: {:#?}", code);
            Ok(CommandResult {
                spinner: Spinner::new(
                    spinners::Spinners::BouncingBall,
                    style("Requesting GitHub token...")
                        .green()
                        .bold()
                        .to_string(),
                ),
                symbol: style("".to_string()).for_stderr().green().to_string(),
                msg: "Finished requesting GitHub token!".to_owned(),
            })
        }
        Err(e) => {
            let error = anyhow!("Failed to get code from channel: {e}");
            Err(error)
        }
    }
}

#[derive(Debug, Serialize, Deserialize)]
struct GithubUser {
    email: Option<String>,
    name: String,
}

#[derive(Debug, Serialize, Deserialize)]
pub struct GithubEmail {
    pub email: String,
    primary: bool,
    verified: bool,
    visibility: Option<String>,
}

impl Display for GithubEmail {
    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
        write!(f, "{}", self.email)
    }
}

pub async fn do_signup<T: Serialize + ?Sized>(args: &T) -> Result<CommandResult> {
    let spinner = Spinner::new(
        spinners::Spinners::BouncingBall,
        style("Signing you up...").green().bold().to_string(),
    );

    let response = Client::new()
        .post(build_smb_signup_url())
        .json(&args)
        .send()
        .await?;

    match response.status() {
        StatusCode::OK => Ok(CommandResult {
            spinner,
            symbol: "".to_owned(),
            msg: "Your account has been created. Check email for verification link.".to_owned(),
        }),
        StatusCode::UNPROCESSABLE_ENTITY => {
            let result: SignupResult = response.json().await?;
            let error = anyhow!("Failed to signup: {}", result.status.message);
            Err(error)
        }
        _ => {
            let error = anyhow!("Failed to signup: {}", response.status());
            Err(error)
        }
    }
}

fn build_smb_signup_url() -> String {
    let mut url_builder = smb_base_url_builder();
    url_builder.add_route("v1/users");
    url_builder.build()
}