license_api/
lib.rs

1use reqwest::StatusCode;
2use std::collections::HashMap;
3use std::error::Error;
4use log::{debug, info};
5use models::{ErrorResponse, LoginRequest, LoginResponse, MeResponse};
6use traits::Authenticator;
7
8pub mod hwid;
9pub mod models;
10pub mod traits;
11
12pub const NOT_LINKED: &str = "not_linked";
13
14pub struct BasicAuthenticator {
15    pub base_url: String,
16    pub client: reqwest::Client,
17}
18
19impl BasicAuthenticator {
20    pub fn new(base_url: impl Into<String>) -> Self {
21        BasicAuthenticator {
22            base_url: base_url.into(),
23            client: reqwest::Client::new(),
24        }
25    }
26}
27
28#[async_trait::async_trait]
29impl Authenticator for BasicAuthenticator {
30    async fn login(
31        &self,
32        creds: &LoginRequest,
33        hwid: &str,
34    ) -> Result<LoginResponse, Box<dyn Error + Send + Sync>> {
35        let url = format!("{}/auth/login", self.base_url.trim_end_matches('/'));
36
37        let resp = self.client.post(&url).form(&creds).send().await?;
38
39        match resp.status() {
40            StatusCode::OK | StatusCode::CREATED => {
41                let lr = resp.json::<LoginResponse>().await?;
42
43                let me = &self.me(&lr.access_token).await?;
44
45                match me.hwid.as_str() {
46                    NOT_LINKED => {
47                        let _ = &self
48                            .link_hwid(&hwid, &lr.access_token)
49                            .await?;
50                        debug!("hwid successfully linked {}", me.username);
51                    }
52                    h if h == hwid => {
53                        debug!("welcome back, {}!", me.username);
54                    }
55                    _ => {
56                        debug!("hwid mismatch! access denied.");
57                    }
58                }
59
60                debug!("login successful");
61
62                Ok(lr)
63            }
64            StatusCode::UNAUTHORIZED | StatusCode::BAD_REQUEST => {
65                let err = resp.json::<ErrorResponse>().await?;
66                Err(format!("{}", err.detail).into())
67            }
68            other => Err(format!("unexpected response status: {}", other).into()),
69        }
70    }
71
72    async fn me(&self, access_token: &str) -> Result<MeResponse, Box<dyn Error + Send + Sync>> {
73        let url = format!("{}/users/me", self.base_url.trim_end_matches('/'));
74
75        let resp = self
76            .client
77            .get(&url)
78            .bearer_auth(access_token)
79            .send()
80            .await?;
81
82        match resp.status() {
83            StatusCode::OK => {
84                let mr = resp.json::<MeResponse>().await?;
85                debug!("getting user info...");
86
87                Ok(mr)
88            }
89            StatusCode::UNAUTHORIZED | StatusCode::BAD_REQUEST => {
90                let err = resp.json::<ErrorResponse>().await?;
91                Err(format!("{}", err.detail).into())
92            }
93            other => Err(format!("unexpected response status: {}", other).into()),
94        }
95    }
96
97    async fn link_hwid(
98        &self,
99        hwid: &str,
100        access_token: &str,
101    ) -> Result<String, Box<dyn Error + Send + Sync>> {
102        let url = format!("{}/users/hwid", self.base_url.trim_end_matches('/'));
103
104        let mut map = HashMap::new();
105        map.insert("value", hwid);
106
107        let resp = self
108            .client
109            .patch(&url)
110            .json(&map)
111            .bearer_auth(access_token)
112            .send()
113            .await?;
114
115        match resp.status() {
116            StatusCode::OK => {
117                debug!("hwid successfully linked");
118
119                Ok(resp.text().await?)
120            },
121            StatusCode::UNAUTHORIZED | StatusCode::BAD_REQUEST => {
122                let err = resp.json::<ErrorResponse>().await?;
123                Err(format!("link failed: {}", err.detail).into())
124            }
125            other => Err(format!("unexpected response status: {}", other).into()),
126        }
127    }
128}