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