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
108impl 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}