trakt_rs/api/
auth.rs

1//! Authentication endpoints
2//!
3//! <https://trakt.docs.apiary.io/#reference/authentication-oauth>
4//! <https://trakt.docs.apiary.io/#reference/authentication-devices>
5
6pub mod token {
7    //! Exchange authorization code for an access & refresh token
8    //!
9    //! <https://trakt.docs.apiary.io/#reference/authentication-oauth/get-token/exchange-code-for-access_token>
10
11    use bytes::BufMut;
12    use trakt_core::{error::IntoHttpError, Context, Metadata};
13
14    #[derive(Debug, Clone, Eq, PartialEq)]
15    pub struct Request {
16        pub code: String,
17        pub client_secret: String,
18        pub redirect_uri: String,
19    }
20
21    impl trakt_core::Request for Request {
22        type Response = Response;
23        const METADATA: Metadata = Metadata {
24            endpoint: "/oauth/token",
25            method: http::Method::POST,
26            auth: trakt_core::AuthRequirement::None,
27        };
28
29        fn try_into_http_request<T: Default + BufMut>(
30            self,
31            ctx: Context,
32        ) -> Result<http::Request<T>, IntoHttpError> {
33            let body = T::default();
34            let mut writer = body.writer();
35
36            let json = serde_json::json!({
37                "code": self.code,
38                "client_id": ctx.client_id,
39                "client_secret": self.client_secret,
40                "redirect_uri": self.redirect_uri,
41                "grant_type": "authorization_code",
42            });
43            serde_json::to_writer(&mut writer, &json)?;
44
45            trakt_core::construct_req(&ctx, &Self::METADATA, &(), &(), writer.into_inner())
46        }
47    }
48
49    #[derive(Debug, Clone, Eq, PartialEq, Hash, serde::Deserialize, trakt_macros::Response)]
50    pub struct Response {
51        pub access_token: String,
52        pub token_type: String,
53        pub expires_in: i64,
54        pub refresh_token: String,
55        pub scope: String,
56        pub created_at: i64,
57    }
58}
59
60pub mod exchange {
61    //! Exchange refresh token for a new access token
62    //!
63    //! <https://trakt.docs.apiary.io/#reference/authentication-oauth/revoke-token/revoke-an-access_token>
64
65    use bytes::BufMut;
66    use trakt_core::{error::IntoHttpError, Context, Metadata};
67
68    #[derive(Debug, Clone, Eq, PartialEq)]
69    pub struct Request {
70        pub refresh_token: String,
71        pub client_secret: String,
72        pub redirect_uri: String,
73    }
74
75    impl trakt_core::Request for Request {
76        type Response = Response;
77        const METADATA: Metadata = Metadata {
78            endpoint: "/oauth/token",
79            method: http::Method::POST,
80            auth: trakt_core::AuthRequirement::None,
81        };
82
83        fn try_into_http_request<T: Default + BufMut>(
84            self,
85            ctx: Context,
86        ) -> Result<http::Request<T>, IntoHttpError> {
87            let body = T::default();
88            let mut writer = body.writer();
89
90            let json = serde_json::json!({
91                "refresh_token": self.refresh_token,
92                "client_id": ctx.client_id,
93                "client_secret": self.client_secret,
94                "redirect_uri": self.redirect_uri,
95                "grant_type": "refresh_token",
96            });
97            serde_json::to_writer(&mut writer, &json)?;
98
99            trakt_core::construct_req(&ctx, &Self::METADATA, &(), &(), writer.into_inner())
100        }
101    }
102
103    #[derive(Debug, Clone, Eq, PartialEq, serde::Deserialize, trakt_macros::Response)]
104    pub struct Response {
105        pub access_token: String,
106        pub token_type: String,
107        pub expires_in: i64,
108        pub refresh_token: String,
109        pub scope: String,
110        pub created_at: i64,
111    }
112}
113
114pub mod revoke {
115    //! Revoke an access token
116    //!
117    //! <https://trakt.docs.apiary.io/#reference/authentication-oauth/revoke-token>
118
119    use bytes::BufMut;
120    use trakt_core::{error::IntoHttpError, Context, Metadata};
121
122    #[derive(Debug, Clone, Eq, PartialEq)]
123    pub struct Request {
124        pub token: String,
125        pub client_secret: String,
126    }
127
128    impl trakt_core::Request for Request {
129        type Response = Response;
130        const METADATA: Metadata = Metadata {
131            endpoint: "/oauth/revoke",
132            method: http::Method::POST,
133            auth: trakt_core::AuthRequirement::None,
134        };
135
136        fn try_into_http_request<T: Default + BufMut>(
137            self,
138            ctx: Context,
139        ) -> Result<http::Request<T>, IntoHttpError> {
140            let body = T::default();
141            let mut writer = body.writer();
142
143            let json = serde_json::json!({
144                "token": self.token,
145                "client_id": ctx.client_id,
146                "client_secret": self.client_secret,
147            });
148            serde_json::to_writer(&mut writer, &json)?;
149
150            trakt_core::construct_req(&ctx, &Self::METADATA, &(), &(), writer.into_inner())
151        }
152    }
153
154    #[derive(Debug, Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Hash, trakt_macros::Response)]
155    pub struct Response;
156}
157
158pub mod device_code {
159    //! Generate a device code
160    //!
161    //! <https://trakt.docs.apiary.io/#reference/authentication-devices/device-code/generate-new-device-codes>
162
163    use bytes::BufMut;
164    use trakt_core::{error::IntoHttpError, Context, Metadata};
165
166    #[derive(Debug, Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Hash, Default)]
167    pub struct Request;
168
169    impl trakt_core::Request for Request {
170        type Response = Response;
171        const METADATA: Metadata = Metadata {
172            endpoint: "/oauth/device/code",
173            method: http::Method::POST,
174            auth: trakt_core::AuthRequirement::None,
175        };
176
177        fn try_into_http_request<T: Default + BufMut>(
178            self,
179            ctx: Context,
180        ) -> Result<http::Request<T>, IntoHttpError> {
181            let body = T::default();
182            let mut writer = body.writer();
183
184            let json = serde_json::json!({
185                "client_id": ctx.client_id,
186            });
187            serde_json::to_writer(&mut writer, &json)?;
188
189            trakt_core::construct_req(&ctx, &Self::METADATA, &(), &(), writer.into_inner())
190        }
191    }
192
193    #[derive(Debug, Clone, Eq, PartialEq, serde::Deserialize, trakt_macros::Response)]
194    pub struct Response {
195        pub device_code: String,
196        pub user_code: String,
197        pub verification_url: String,
198        pub expires_in: i64,
199        pub interval: i64,
200    }
201}
202
203pub mod poll_token {
204    //! Poll for an access token
205    //!
206    //! <https://trakt.docs.apiary.io/#reference/authentication-devices/device-code/poll-for-the-access_token>
207
208    use bytes::BufMut;
209    use trakt_core::{error::IntoHttpError, Context, Metadata};
210
211    #[derive(Debug, Clone, Eq, PartialEq)]
212    pub struct Request {
213        pub device_code: String,
214        pub client_secret: String,
215    }
216
217    impl trakt_core::Request for Request {
218        type Response = Response;
219        const METADATA: Metadata = Metadata {
220            endpoint: "/oauth/device/token",
221            method: http::Method::POST,
222            auth: trakt_core::AuthRequirement::None,
223        };
224
225        fn try_into_http_request<T: Default + BufMut>(
226            self,
227            ctx: Context,
228        ) -> Result<http::Request<T>, IntoHttpError> {
229            let body = T::default();
230            let mut writer = body.writer();
231
232            let json = serde_json::json!({
233                "code": self.device_code,
234                "client_id": ctx.client_id,
235                "client_secret": self.client_secret,
236            });
237            serde_json::to_writer(&mut writer, &json)?;
238
239            trakt_core::construct_req(&ctx, &Self::METADATA, &(), &(), writer.into_inner())
240        }
241    }
242
243    /// Poll Response
244    ///
245    /// Will [`ApiError::BadRequest`] if the device code has not been authorized by the user yet.
246    ///
247    /// [`ApiError::BadRequest`]: crate::error::ApiError::BadRequest
248    #[derive(Debug, Clone, Eq, PartialEq, serde::Deserialize, trakt_macros::Response)]
249    pub struct Response {
250        pub access_token: String,
251        pub token_type: String,
252        pub expires_in: i64,
253        pub refresh_token: String,
254        pub scope: String,
255        pub created_at: i64,
256    }
257}
258
259#[cfg(test)]
260mod tests {
261    use serde_json::json;
262    use trakt_core::Context;
263
264    use super::*;
265    use crate::test::assert_request;
266
267    const CTX: Context = Context {
268        base_url: "https://api.trakt.tv",
269        client_id: "client_id",
270        oauth_token: None,
271    };
272
273    #[test]
274    fn test_token_request() {
275        let expected = json!({
276            "code": "code",
277            "client_id": CTX.client_id,
278            "client_secret": "secret",
279            "redirect_uri": "https://localhost:8080",
280            "grant_type": "authorization_code",
281        });
282        let req = token::Request {
283            code: "code".to_owned(),
284            client_secret: "secret".to_owned(),
285            redirect_uri: "https://localhost:8080".to_owned(),
286        };
287        assert_request(CTX, req, "https://api.trakt.tv/oauth/token", &expected);
288    }
289
290    #[test]
291    fn test_exchange_request() {
292        let expected = json!({
293            "refresh_token": "token",
294            "client_id": CTX.client_id,
295            "client_secret": "secret",
296            "redirect_uri": "https://localhost:8080",
297            "grant_type": "refresh_token",
298        });
299        let req = exchange::Request {
300            refresh_token: "token".to_owned(),
301            client_secret: "secret".to_owned(),
302            redirect_uri: "https://localhost:8080".to_owned(),
303        };
304        assert_request(CTX, req, "https://api.trakt.tv/oauth/token", &expected);
305    }
306
307    #[test]
308    fn test_revoke_request() {
309        let expected = json!({
310            "token": "token",
311            "client_id": CTX.client_id,
312            "client_secret": "secret",
313        });
314        let req = revoke::Request {
315            token: "token".to_owned(),
316            client_secret: "secret".to_owned(),
317        };
318        assert_request(CTX, req, "https://api.trakt.tv/oauth/revoke", &expected);
319    }
320
321    #[test]
322    fn test_device_code_request() {
323        let expected = json!({
324            "client_id": CTX.client_id,
325        });
326        let req = device_code::Request;
327        assert_request(
328            CTX,
329            req,
330            "https://api.trakt.tv/oauth/device/code",
331            &expected,
332        );
333    }
334
335    #[test]
336    fn test_poll_token_request() {
337        let expected = json!({
338            "code": "code",
339            "client_id": CTX.client_id,
340            "client_secret": "secret",
341        });
342        let req = poll_token::Request {
343            device_code: "code".to_owned(),
344            client_secret: "secret".to_owned(),
345        };
346        assert_request(
347            CTX,
348            req,
349            "https://api.trakt.tv/oauth/device/token",
350            &expected,
351        );
352    }
353}