Skip to main content

other_pocket/
auth.rs

1use crate::client::PocketClient;
2use crate::errors::PocketError;
3use crate::Pocket;
4use crate::PocketResult;
5use serde::{Deserialize, Serialize};
6use url::Url;
7
8#[derive(Serialize)]
9pub struct PocketOAuthRequest<'a> {
10    consumer_key: &'a str,
11    redirect_uri: &'a str,
12    state: Option<&'a str>,
13}
14
15#[derive(Deserialize, Debug, PartialEq)]
16pub struct PocketOAuthResponse {
17    code: String,
18    state: Option<String>,
19}
20
21#[derive(Serialize)]
22pub struct PocketAuthorizeRequest<'a> {
23    consumer_key: &'a str,
24    code: &'a str,
25}
26
27#[derive(Deserialize, Debug, PartialEq)]
28pub struct PocketAuthorizeResponse {
29    access_token: String,
30    username: String,
31    state: Option<String>,
32}
33
34pub struct PocketAuthentication {
35    consumer_key: String,
36    redirect_uri: String,
37    client: PocketClient,
38}
39
40impl PocketAuthentication {
41    pub fn new(consumer_key: &str, redirect_uri: &str) -> PocketAuthentication {
42        PocketAuthentication {
43            consumer_key: consumer_key.to_string(),
44            redirect_uri: redirect_uri.to_string(),
45            client: PocketClient::new(),
46        }
47    }
48
49    pub async fn request(&self, state: Option<&str>) -> PocketResult<String> {
50        let body = &PocketOAuthRequest {
51            consumer_key: &self.consumer_key,
52            redirect_uri: &self.redirect_uri,
53            state,
54        };
55
56        self.client
57            .post("https://getpocket.com/v3/oauth/request", &body)
58            .await
59            .and_then(|r: PocketOAuthResponse| {
60                PocketAuthentication::verify_state(state, r.state.as_deref()).map(|()| r.code)
61            })
62    }
63
64    fn verify_state(request_state: Option<&str>, response_state: Option<&str>) -> PocketResult<()> {
65        match (request_state, response_state) {
66            (Some(s1), Some(s2)) if s1 == s2 => Ok(()),
67            (None, None) => Ok(()),
68            _ => Err(PocketError::Proto(0, "State does not match".to_string())),
69        }
70    }
71
72    pub fn authorize_url(&self, code: &str) -> Url {
73        let params = vec![
74            ("request_token", code),
75            ("redirect_uri", &self.redirect_uri),
76        ];
77        let mut url = Url::parse("https://getpocket.com/auth/authorize").unwrap();
78        url.query_pairs_mut().extend_pairs(params.into_iter());
79        url
80    }
81
82    pub async fn authorize(&self, code: &str, state: Option<&str>) -> PocketResult<PocketUser> {
83        let body = &PocketAuthorizeRequest {
84            consumer_key: &self.consumer_key,
85            code,
86        };
87
88        self.client
89            .post("https://getpocket.com/v3/oauth/authorize", &body)
90            .await
91            .and_then(|r: PocketAuthorizeResponse| {
92                PocketAuthentication::verify_state(state, r.state.as_deref()).map(|()| PocketUser {
93                    consumer_key: self.consumer_key.clone(),
94                    access_token: r.access_token,
95                    username: r.username,
96                })
97            })
98    }
99}
100
101#[derive(Debug)]
102pub struct PocketUser {
103    pub consumer_key: String,
104    pub access_token: String,
105    pub username: String,
106}
107
108// TODO - change this to a Into and move to Pocket
109impl PocketUser {
110    pub fn pocket(self) -> Pocket {
111        Pocket::new(&self.consumer_key, &self.access_token)
112    }
113}
114
115#[cfg(test)]
116mod test {
117    use super::*;
118    use crate::utils::remove_whitespace;
119
120    #[test]
121    fn test_serialize_auth_request() {
122        let request = &PocketOAuthRequest {
123            consumer_key: "consumer_key",
124            redirect_uri: "http://localhost",
125            state: Some("state"),
126        };
127
128        let actual = serde_json::to_string(request).unwrap();
129
130        let expected = remove_whitespace(&format!(
131            r#"
132                    {{
133                        "consumer_key": "{consumer_key}",
134                        "redirect_uri": "{redirect_uri}",
135                        "state": "{state}"
136                    }}
137               "#,
138            consumer_key = request.consumer_key,
139            redirect_uri = request.redirect_uri,
140            state = request.state.unwrap()
141        ));
142
143        assert_eq!(actual, expected);
144    }
145
146    #[test]
147    fn test_deserialize_auth_response() {
148        let expected = PocketOAuthResponse {
149            code: "code".to_string(),
150            state: Some("state".to_string()),
151        };
152        let response = remove_whitespace(&format!(
153            r#"
154                    {{
155                        "code": "{code}",
156                        "state": "{state}"
157                    }}
158               "#,
159            code = expected.code,
160            state = expected.state.as_ref().unwrap()
161        ));
162
163        let actual: PocketOAuthResponse = serde_json::from_str(&response).unwrap();
164
165        assert_eq!(actual, expected);
166    }
167
168    #[test]
169    fn test_serialize_authorize_request() {
170        let request = &PocketAuthorizeRequest {
171            consumer_key: "consumer_key",
172            code: "code",
173        };
174
175        let actual = serde_json::to_string(request).unwrap();
176
177        let expected = remove_whitespace(&format!(
178            r#"
179                    {{
180                        "consumer_key": "{consumer_key}",
181                        "code": "{code}"
182                    }}
183               "#,
184            consumer_key = request.consumer_key,
185            code = request.code
186        ));
187
188        assert_eq!(actual, expected);
189    }
190
191    #[test]
192    fn test_deserialize_authorize_response() {
193        let expected = PocketAuthorizeResponse {
194            access_token: "access_token".to_string(),
195            username: "username".to_string(),
196            state: None,
197        };
198        let response = remove_whitespace(&format!(
199            r#"
200                    {{
201                        "access_token": "{access_token}",
202                        "username": "{username}"
203                    }}
204               "#,
205            access_token = expected.access_token,
206            username = expected.username
207        ));
208
209        let actual: PocketAuthorizeResponse = serde_json::from_str(&response).unwrap();
210
211        assert_eq!(actual, expected);
212    }
213}