smbpndk_cli/account/signup/
process.rs

1use super::SignupMethod;
2use crate::account::{
3    lib::authorize_github,
4    model::{Data, Status},
5};
6use anyhow::{anyhow, Result};
7use console::{style, Term};
8use dialoguer::{theme::ColorfulTheme, Input, Password, Select};
9use log::debug;
10use reqwest::{Client, StatusCode};
11use serde::{Deserialize, Serialize};
12use smbpndk_model::CommandResult;
13
14use smbpndk_networking::{smb_base_url_builder, smb_token_file_path};
15use smbpndk_utils::email_validation;
16use spinners::Spinner;
17use std::fmt::{Display, Formatter};
18
19pub struct SignupArgs {
20    pub email: String,
21    pub password: Option<String>,
22    pub password_confirmation: Option<String>,
23    pub authorizations_attributes: Vec<Provider>,
24}
25
26#[derive(Debug, Serialize)]
27pub struct Provider {
28    pub uid: String,
29    pub provider: i8,
30}
31
32#[derive(Debug, Serialize)]
33pub struct SignupGithubParams {
34    pub user: SignupUserGithub,
35}
36
37#[derive(Debug, Serialize)]
38pub struct SignupEmailParams {
39    pub user: SignupUserEmail,
40}
41
42#[derive(Debug, Serialize)]
43pub struct SignupUserGithub {
44    pub email: String,
45    pub authorizations_attributes: Vec<Provider>,
46}
47
48#[derive(Debug, Serialize)]
49pub struct SignupUserEmail {
50    pub email: String,
51    pub password: String,
52}
53
54#[derive(Debug, Serialize, Deserialize)]
55struct SignupResult {
56    status: Status,
57    data: Option<Data>,
58}
59
60pub async fn process_signup() -> Result<CommandResult> {
61    // Check if token file exists
62    if smb_token_file_path().is_some() {
63        return Ok(CommandResult {
64            spinner: Spinner::new(
65                spinners::Spinners::SimpleDotsScrolling,
66                style("Loading...").green().bold().to_string(),
67            ),
68            symbol: "✅".to_owned(),
69            msg: "You are already logged in. Please logout first.".to_owned(),
70        });
71    }
72
73    let signup_methods = vec![SignupMethod::Email, SignupMethod::GitHub];
74    let selection = Select::with_theme(&ColorfulTheme::default())
75        .items(&signup_methods)
76        .default(0)
77        .interact_on_opt(&Term::stderr())
78        .map(|i| signup_methods[i.unwrap()])
79        .unwrap();
80
81    match selection {
82        SignupMethod::Email => signup_with_email(None).await,
83        SignupMethod::GitHub => signup_with_github().await,
84    }
85}
86
87pub async fn signup_with_email(email: Option<String>) -> Result<CommandResult> {
88    let email = if let Some(email) = email {
89        email
90    } else {
91        Input::<String>::with_theme(&ColorfulTheme::default())
92            .with_prompt("Username")
93            .validate_with(|email: &String| email_validation(email))
94            .interact()
95            .unwrap()
96    };
97
98    let password = Password::with_theme(&ColorfulTheme::default())
99        .with_prompt("Password")
100        .validate_with(|input: &String| -> Result<(), &str> {
101            if input.len() >= 6 {
102                Ok(())
103            } else {
104                Err("Password must be at least 6 characters")
105            }
106        })
107        .with_confirmation("Confirm password", "Passwords do not match")
108        .interact()
109        .unwrap();
110
111    let spinner = Spinner::new(
112        spinners::Spinners::BouncingBall,
113        style("Signing up...").green().bold().to_string(),
114    );
115
116    let params = SignupEmailParams {
117        user: SignupUserEmail { email, password },
118    };
119
120    match do_signup(&params).await {
121        Ok(_) => Ok(CommandResult {
122            spinner,
123            symbol: style("✅".to_string()).for_stderr().green().to_string(),
124            msg: "You are signed up! Check your email to confirm your account.".to_owned(),
125        }),
126        Err(e) => Ok(CommandResult {
127            spinner,
128            symbol: style("✘".to_string()).for_stderr().red().to_string(),
129            msg: format!("{e}"),
130        }),
131    }
132}
133
134async fn signup_with_github() -> Result<CommandResult> {
135    match authorize_github().await {
136        Ok(code) => {
137            debug!("Code: {:#?}", code);
138            Ok(CommandResult {
139                spinner: Spinner::new(
140                    spinners::Spinners::BouncingBall,
141                    style("Requesting GitHub token...")
142                        .green()
143                        .bold()
144                        .to_string(),
145                ),
146                symbol: style("✅".to_string()).for_stderr().green().to_string(),
147                msg: "Finished requesting GitHub token!".to_owned(),
148            })
149        }
150        Err(e) => {
151            let error = anyhow!("Failed to get code from channel: {e}");
152            Err(error)
153        }
154    }
155}
156
157#[derive(Debug, Serialize, Deserialize)]
158struct GithubUser {
159    email: Option<String>,
160    name: String,
161}
162
163#[derive(Debug, Serialize, Deserialize)]
164pub struct GithubEmail {
165    pub email: String,
166    primary: bool,
167    verified: bool,
168    visibility: Option<String>,
169}
170
171impl Display for GithubEmail {
172    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
173        write!(f, "{}", self.email)
174    }
175}
176
177pub async fn do_signup<T: Serialize + ?Sized>(args: &T) -> Result<CommandResult> {
178    let spinner = Spinner::new(
179        spinners::Spinners::BouncingBall,
180        style("Signing you up...").green().bold().to_string(),
181    );
182
183    let response = Client::new()
184        .post(build_smb_signup_url())
185        .json(&args)
186        .send()
187        .await?;
188
189    match response.status() {
190        StatusCode::OK => Ok(CommandResult {
191            spinner,
192            symbol: "✅".to_owned(),
193            msg: "Your account has been created. Check email for verification link.".to_owned(),
194        }),
195        StatusCode::UNPROCESSABLE_ENTITY => {
196            let result: SignupResult = response.json().await?;
197            let error = anyhow!("Failed to signup: {}", result.status.message);
198            Err(error)
199        }
200        _ => {
201            let error = anyhow!("Failed to signup: {}", response.status());
202            Err(error)
203        }
204    }
205}
206
207fn build_smb_signup_url() -> String {
208    let mut url_builder = smb_base_url_builder();
209    url_builder.add_route("v1/users");
210    url_builder.build()
211}