headhunter_bindings/
authentication.rs

1use std::borrow::Cow;
2use std::time::Duration;
3use thirtyfour::{By, DesiredCapabilities, WebDriver};
4use url::Url;
5
6use super::{Error, Result, request::*, response::*};
7
8/// Represents the application's credentials, which can be obtained from https://dev.hh.ru
9pub struct ApplicationCredentials<'a> {
10    pub client_id: &'a str,
11    pub client_secret: &'a str,
12}
13
14/// Represents user credentials for the http://hh.ru website
15pub struct UserCredentials<'a> {
16    pub login: &'a str,
17    pub password: &'a str,
18}
19
20/// Helper structure for obtaining token using authorization
21/// through credentials in Selenium - browser automation software
22pub struct AuthenticationClient;
23
24impl AuthenticationClient {
25    const SERVER_URL: &'static str = "http://localhost:9515";
26    const BASE_URL: &'static str = "https://hh.ru";
27
28    /// Creates new `AuthenticationClient`
29    #[allow(clippy::new_without_default)]
30    pub fn new() -> Self {
31        Self
32    }
33
34    /// Enters application and user data through Selenium to get temporary code
35    pub async fn get_authorization_code(
36        &self,
37        application: &ApplicationCredentials<'_>,
38        user: &UserCredentials<'_>,
39    ) -> Result<String> {
40        let driver = WebDriver::new(Self::SERVER_URL, DesiredCapabilities::chrome()).await?;
41
42        let request = UserAuthorizationRequest {
43            response_type: "code",
44            client_id: application.client_id,
45        };
46
47        let mut url = Url::parse(Self::BASE_URL)?;
48        url.set_path(UserAuthorizationRequest::method().ok_or_else(|| Error::UrlBuild)?);
49
50        let query = serde_urlencoded::to_string(request)?;
51        url.set_query(Some(&query));
52
53        driver.goto(url.as_str()).await?;
54
55        let elem_form = driver
56            .find(By::XPath("//*[@data-qa='account-login-form']"))
57            .await?;
58
59        let elem_expand_login_by_password = elem_form
60            .find(By::XPath("//*[@data-qa='expand-login-by_password']"))
61            .await?;
62
63        elem_expand_login_by_password.click().await?;
64
65        let elem_login = elem_form
66            .find(By::XPath("//*[@data-qa='login-input-username']"))
67            .await?;
68        let elem_password = elem_form
69            .find(By::XPath("//*[@data-qa='login-input-password']"))
70            .await?;
71
72        let elem_button = elem_form.find(By::Css("button[type='submit']")).await?;
73
74        elem_login.send_keys(user.login).await?;
75        elem_password.send_keys(user.password).await?;
76
77        elem_button.click().await?;
78
79        tokio::time::sleep(Duration::from_secs(3)).await;
80
81        let url = driver.current_url().await?;
82        let code = url
83            .query_pairs()
84            .find(|(key, _)| key == &Cow::Borrowed("code"))
85            .map(|(_, value)| value)
86            .expect("could not find authorization_code")
87            .to_string();
88
89        driver.quit().await?;
90
91        Ok(code)
92    }
93
94    async fn request<Req: Request>(&self, req: &Req) -> Result<Req::Response> {
95        let mut url = Url::parse(Self::BASE_URL)?;
96        url.set_path(Req::method().ok_or_else(|| Error::UrlBuild)?);
97
98        let client = reqwest::Client::new();
99        let response = client
100            .post(url)
101            .form(&req)
102            .send()
103            .await?
104            .json::<Req::Response>()
105            .await?;
106
107        Ok(response)
108    }
109
110    /// Continues the authorization process, receives access token from the temporary code
111    pub async fn perform_authentication(
112        &self,
113        application: &ApplicationCredentials<'_>,
114        authorization_code: &str,
115    ) -> Result<UserOpenAuthorizationResponse> {
116        self.request(&UserOpenAuthorizationRequest {
117            grant_type: "authorization_code",
118            client_id: application.client_id,
119            client_secret: application.client_secret,
120            code: authorization_code,
121        })
122        .await
123    }
124
125    /// Creates an access token renewal request
126    pub async fn refresh_token(
127        &self,
128        refresh_token: &str,
129    ) -> Result<UserOpenAuthorizationResponse> {
130        self.request(&UserRenewOpenAuthorizationRequest {
131            grant_type: "refresh_token",
132            refresh_token,
133        })
134        .await
135    }
136}