authy_rs/
client.rs

1use phonenumber::PhoneNumber;
2use reqwest::header::HeaderName;
3use reqwest::RequestBuilder;
4
5use crate::error::{ApiResult, AuthyErr};
6
7
8pub struct Client {
9    http: reqwest::Client,
10    api_key: String
11}
12
13pub enum Via {
14    SMS,
15    Call
16}
17
18impl Into<&str> for Via {
19    fn into(self) -> &'static str {
20        match self {
21            Via::SMS => "sms",
22            Via::Call => "call"
23        }
24    }
25}
26
27
28#[derive(Serialize, Debug)]
29struct VerifyRequest<'a> {
30    phone_number: &'a str,
31    country_code: &'a str,
32    via: &'a str,
33    code_length: u8,
34    locale: &'a str
35}
36
37#[derive(Deserialize, Debug)]
38pub struct VerifyResponse {
39    carrier: String,
40    is_cellphone: bool,
41    message: String,
42    seconds_to_expire: u32,
43    uuid: String,
44    success: bool
45}
46
47#[derive(Serialize, Debug)]
48pub struct CheckRequest<'a> {
49    phone_number: &'a str,
50    country_code: &'a str,
51    verification_code: u32
52}
53
54#[derive(Deserialize, Debug)]
55pub struct CheckResponse {
56    message: String,
57    success: bool
58}
59
60impl Into<bool> for CheckResponse {
61    fn into(self) -> bool {
62        self.success
63    }
64}
65
66#[derive(Serialize, Debug)]
67pub struct StatusRequest<'a> {
68    uuid: &'a str
69}
70
71enum Status {
72    Expired,
73    Verified,
74    Pending,
75    Unknown
76}
77
78impl<'a> From<&'a str> for Status {
79    fn from(value: &'a str) -> Status {
80        match value {
81            "expired" => Status::Expired,
82            "verified" => Status::Verified,
83            "pending" => Status::Pending,
84            _ => Status::Unknown
85        }
86    }
87}
88
89#[derive(Deserialize, Debug)]
90pub struct StatusResponse {
91    status: String,
92    seconds_to_expire: u32,
93    success: bool,
94    message: String
95}
96
97
98
99impl Client {
100    pub fn new(api_key: &str) -> Client {
101        Client {
102            http: reqwest::Client::new(),
103            api_key: api_key.into()
104        }
105    }
106
107    fn query<T, S>(&self, rb: RequestBuilder, data: &T) -> Result<S, AuthyErr>
108        where T: serde::Serialize,
109        for<'de> S: serde::Deserialize<'de>
110    {
111        let x_authy_api_key: HeaderName = HeaderName::from_static("x-authy-api-key");
112
113        rb.header(x_authy_api_key, self.api_key.clone())
114            .form(data)
115            .send()
116            .map_err(AuthyErr::Http)
117            .and_then(|mut r: reqwest::Response| {
118                r.json()
119                    .map_err(AuthyErr::Http)
120                    .and_then(|r: ApiResult<S>| match r {
121                            ApiResult::Ok(x) => Ok(x),
122                            ApiResult::Err(x) => Err(x)
123                        }.map_err(AuthyErr::Api))
124            })
125    }
126
127    pub fn verify(&self, number: &PhoneNumber, via: Via, code_length: u8, locale: &str) -> Result<VerifyResponse, AuthyErr> {
128        self.query::<VerifyRequest, VerifyResponse>(
129            self.http.post("https://api.authy.com/protected/json/phones/verification/start"),
130            &VerifyRequest {
131                    phone_number: &number.national().to_string(),
132                    country_code: &number.code().value().to_string(),
133                    via: via.into(),
134                    code_length,
135                    locale,
136                })
137    }
138
139    pub fn check(&self, number: &PhoneNumber, verification_code: u32) -> Result<CheckResponse, AuthyErr> {
140        self.query::<CheckRequest, CheckResponse>(
141            self.http.get("https://api.authy.com/protected/json/phones/verification/check"),
142            &CheckRequest {
143                    phone_number: &number.national().to_string(),
144                    country_code: &number.code().value().to_string(),
145                    verification_code
146                })
147    }
148
149    pub fn status(&self, uuid: &str) -> Result<StatusResponse, AuthyErr> {
150        self.query::<StatusRequest, StatusResponse>(
151            self.http.get("https://api.authy.com/protected/json/phones/verification/status"),
152            &StatusRequest { uuid }
153        )
154    }
155}
156
157
158#[cfg(test)]
159mod tests {
160    use crate::client::{Client, Via};
161    use phonenumber::PhoneNumber;
162
163    fn setup() -> (Client, PhoneNumber) {
164        dotenv::dotenv()
165            .expect("No .env file found");
166        (Client::new(&dotenv::var("VERIFY_API_KEY")
167            .expect("No verify API key in .env file")),
168         phonenumber::parse(None, dotenv::var("VERIFY_TEST_PHONE_NUMBER")
169             .expect("No verify test phone number in .env file"))
170             .expect("Can't parse test phone number"))
171    }
172
173
174    #[test]
175    fn test_verify() {
176        let (client, test_phone_number) = setup();
177        println!("Response:\n{:#?}", client.verify(&test_phone_number, Via::SMS, 6, "de")
178            .expect("verify error"));
179    }
180
181    #[test]
182    fn test_check() {
183        let verification_code: Option<u32> = None;
184        let verification_code = verification_code.expect("You must set the verification code here to get a positive result");
185
186        let (client, test_phone_number) = setup();
187        println!("Response:\n{:#?}", client.check(&test_phone_number, verification_code)
188            .expect("check error"));
189    }
190
191    #[test]
192    fn test_status() {
193        let uuid: Option<&str> = None;
194        let uuid = uuid.expect("You must set the UUID for a verification request here to get a positive result");
195
196        let (client, _) = setup();
197        println!("Response:\n{:#?}", client.status(uuid));
198    }
199}