mangadex_api/v5/oauth/
login.rs

1//! Builder for the OAuth login endpoint.
2//!
3//! <https://api.mangadex.org/docs/02-authentication/>
4//!
5//! It's the support for [Personal Client](https://api.mangadex.org/docs/02-authentication/personal-clients/)
6//!
7//! # Examples
8//!
9//! ```rust
10//! use mangadex_api_types::{Password, Username};
11//! use mangadex_api::v5::MangaDexClient;
12//! use mangadex_api_schema::v5::oauth::ClientInfo;
13//!
14//! # async fn run() -> anyhow::Result<()> {
15//!
16//! let mut client = MangaDexClient::default();
17//!
18//! client.set_client_info(&ClientInfo {
19//!     client_id: "someClientId".to_string(),
20//!     client_secret: "someClientSecret".to_string()
21//! }).await?;
22//!
23//! let login_res = client
24//!     .oauth()
25//!     .login()
26//!     .username(Username::parse("myusername")?)
27//!     .password(Password::parse("hunter2")?)
28//!     .send()
29//!     .await?;
30//!
31//! println!("login: {:?}", login_res);
32//! # Ok(())
33//! # }
34//! ```
35
36use derive_builder::Builder;
37use mangadex_api_schema::v5::oauth::OAuthTokenResponse;
38use mangadex_api_schema::v5::AuthTokens;
39use mangadex_api_types::oauth::GrantTypeSupported;
40use reqwest::Method;
41use serde::Serialize;
42#[cfg(not(test))]
43use url::Url;
44
45use crate::v5::HttpClientRef;
46use mangadex_api_types::error::Result;
47use mangadex_api_types::{Password, Username};
48
49/// Log into an account.
50///
51/// Makes a request to `POST https://auth.mangadex.org/realms/mangadex/protocol/openid-connect/token`.
52#[cfg_attr(
53    feature = "deserializable-endpoint",
54    derive(serde::Deserialize, getset::Getters, getset::Setters)
55)]
56#[derive(Debug, Clone, Builder)]
57#[builder(
58    setter(into, strip_option),
59    build_fn(error = "mangadex_api_types::error::BuilderError")
60)]
61pub struct RetriveTokens {
62    /// This should never be set manually as this is only for internal use.
63    #[doc(hidden)]
64    #[cfg_attr(feature = "deserializable-endpoint", serde(skip))]
65    #[builder(pattern = "immutable")]
66    #[cfg_attr(feature = "deserializable-endpoint", getset(set = "pub", get = "pub"))]
67    pub http_client: HttpClientRef,
68
69    pub username: Username,
70
71    pub password: Password,
72}
73
74#[derive(Clone, Serialize)]
75struct RetriveTokenBody {
76    grant_type: GrantTypeSupported,
77    username: Username,
78    password: Password,
79    client_id: String,
80    client_secret: String,
81}
82
83impl RetriveTokens {
84    pub async fn send(&mut self) -> Result<OAuthTokenResponse> {
85        let res = {
86            let client = {
87                #[cfg(all(
88                    not(feature = "multi-thread"),
89                    not(feature = "tokio-multi-thread"),
90                    not(feature = "rw-multi-thread")
91                ))]
92                {
93                    &self.http_client.try_borrow()?
94                }
95                #[cfg(any(feature = "multi-thread", feature = "tokio-multi-thread"))]
96                {
97                    &self.http_client.lock().await
98                }
99                #[cfg(feature = "rw-multi-thread")]
100                {
101                    &self.http_client.read().await
102                }
103            };
104            let client_info = client
105                .get_client_info()
106                .ok_or(mangadex_api_types::error::Error::MissingClientInfo)?;
107            let params = RetriveTokenBody {
108                grant_type: GrantTypeSupported::Password,
109                username: self.username.to_owned(),
110                password: self.password.to_owned(),
111                client_id: client_info.client_id.to_owned(),
112                client_secret: client_info.client_secret.to_owned(),
113            };
114            #[cfg(test)]
115            let res = client
116                .client
117                .request(
118                    Method::POST,
119                    client
120                        .base_url
121                        .join("/realms/mangadex/protocol/openid-connect/token")?,
122                )
123                .form(&params)
124                .send()
125                .await?;
126            #[cfg(not(test))]
127            let res = client
128                .client
129                .request(
130                    Method::POST,
131                    Url::parse(crate::AUTH_URL)?
132                        .join("/realms/mangadex/protocol/openid-connect/token")?,
133                )
134                .form(&params)
135                .send()
136                .await?;
137            res.json::<OAuthTokenResponse>().await?
138        };
139        {
140            let auth_tokens: AuthTokens = From::from(res.clone());
141            let client = {
142                #[cfg(all(
143                    not(feature = "multi-thread"),
144                    not(feature = "tokio-multi-thread"),
145                    not(feature = "rw-multi-thread")
146                ))]
147                {
148                    &mut self.http_client.try_borrow_mut()?
149                }
150                #[cfg(any(feature = "multi-thread", feature = "tokio-multi-thread"))]
151                {
152                    &mut self.http_client.lock().await
153                }
154                #[cfg(feature = "rw-multi-thread")]
155                {
156                    &mut self.http_client.write().await
157                }
158            };
159            client.set_auth_tokens(&auth_tokens);
160        };
161        Ok(res)
162    }
163}
164
165builder_send! {
166    #[builder] RetriveTokensBuilder,
167    OAuthTokenResponse
168}
169
170#[cfg(test)]
171mod tests {
172    use mangadex_api_schema::v5::oauth::ClientInfo;
173    use mangadex_api_types::oauth::GrantTypeSupported;
174    use serde_json::json;
175    use url::Url;
176    use wiremock::matchers::{body_string, header, method, path};
177    use wiremock::{Mock, MockServer, ResponseTemplate};
178
179    use crate::v5::oauth::login::RetriveTokenBody;
180    use crate::v5::AuthTokens;
181    use crate::{HttpClient, MangaDexClient};
182    use mangadex_api_types::{Password, Username};
183    use serde_urlencoded::to_string;
184
185    #[tokio::test]
186    async fn login_fires_a_request_to_base_url() -> anyhow::Result<()> {
187        let mock_server = MockServer::start().await;
188        let http_client: HttpClient = HttpClient::builder()
189            .base_url(Url::parse(&mock_server.uri())?)
190            .build()?;
191        let mut mangadex_client = MangaDexClient::new_with_http_client(http_client);
192
193        let client_info: ClientInfo = ClientInfo {
194            client_id: "someClientId".to_string(),
195            client_secret: "someClientSecret".to_string(),
196        };
197
198        mangadex_client.set_client_info(&client_info).await?;
199
200        let username = Username::parse("myusername")?;
201
202        let password = Password::parse("mypassword")?;
203
204        let auth_tokens = AuthTokens {
205            session: "sessiontoken".to_string(),
206            refresh: "refreshtoken".to_string(),
207        };
208
209        let response_body = json!({
210            "access_token": auth_tokens.session.clone(),
211            "expires_in": 900,
212            "refresh_expires_in": 2414162,
213            "refresh_token": auth_tokens.refresh.clone(),
214            "token_type": "Bearer",
215            "not-before-policy": 0,
216            "session_state": "c176499d-6e8d-4ddf-ad59-6d922be66431",
217            "scope": "groups email profile",
218            "client_type": "personal"
219        });
220        let expected_body: String = to_string(RetriveTokenBody {
221            grant_type: GrantTypeSupported::Password,
222            username: username.clone(),
223            password: password.clone(),
224            client_id: client_info.client_id.clone(),
225            client_secret: client_info.client_secret.clone(),
226        })?;
227
228        Mock::given(method("POST"))
229            .and(path(r"/realms/mangadex/protocol/openid-connect/token"))
230            .and(header("Content-Type", "application/x-www-form-urlencoded"))
231            .and(body_string(expected_body))
232            .respond_with(ResponseTemplate::new(200).set_body_json(response_body))
233            .expect(1)
234            .mount(&mock_server)
235            .await;
236
237        let _ = mangadex_client
238            .oauth()
239            .login()
240            .username(username.clone())
241            .password(password.clone())
242            .send()
243            .await?;
244
245        #[cfg(all(
246            not(feature = "multi-thread"),
247            not(feature = "tokio-multi-thread"),
248            not(feature = "rw-multi-thread")
249        ))]
250        assert_eq!(
251            mangadex_client.http_client.try_borrow()?.get_tokens(),
252            Some(&auth_tokens)
253        );
254        #[cfg(any(feature = "multi-thread", feature = "tokio-multi-thread"))]
255        assert_eq!(
256            mangadex_client.http_client.lock().await.get_tokens(),
257            Some(&auth_tokens)
258        );
259        #[cfg(feature = "rw-multi-thread")]
260        assert_eq!(
261            mangadex_client.http_client.read().await.get_tokens(),
262            Some(&auth_tokens)
263        );
264
265        Ok(())
266    }
267}