Skip to main content

huawei_dongle_api/api/
auth.rs

1//! Authentication API endpoints
2
3use crate::{
4    auth::PasswordEncoder,
5    client::Client,
6    error::{Error, Result},
7    models::{auth::*, common::Response},
8};
9use tracing::{debug, trace};
10
11/// Authentication API for login/logout operations
12pub struct AuthApi<'a> {
13    client: &'a Client,
14}
15
16impl<'a> AuthApi<'a> {
17    pub fn new(client: &'a Client) -> Self {
18        Self { client }
19    }
20
21    /// This endpoint does not require authentication.
22    /// Returns information about password encoding requirements and current login status.
23    pub async fn state_login(&self) -> Result<LoginState> {
24        debug!("Fetching login state");
25
26        let response = self.client.get("/api/user/state-login").await?;
27        let text = response.text().await?;
28
29        trace!("Login state response: {}", text);
30
31        let state: LoginState = serde_xml_rs::from_str(&text)
32            .map_err(|e| Error::generic(format!("Failed to parse login state: {e}")))?;
33
34        debug!(
35            "Login state: {} (password_type: {})",
36            if state.is_logged_in() {
37                "logged in"
38            } else {
39                "not logged in"
40            },
41            state.password_type
42        );
43
44        Ok(state)
45    }
46
47    /// This endpoint requires a valid CSRF token but not authentication.
48    /// Password will be automatically encoded based on the device requirements.
49    pub async fn login(&self, username: &str, password: &str) -> Result<()> {
50        debug!("Attempting login for user: {}", username);
51
52        let login_state = self.state_login().await?;
53
54        if login_state.is_logged_in() {
55            debug!("User is already logged in");
56            return Ok(());
57        }
58
59        if login_state.is_locked() {
60            return Err(Error::session(format!(
61                "Account is locked. Wait time: {} seconds",
62                login_state.remain_wait_time
63            )));
64        }
65
66        let encoded_password = PasswordEncoder::encode_password(password, &login_state);
67
68        let request = LoginRequest::new(
69            username.to_string(),
70            encoded_password,
71            login_state.password_type.clone(),
72        );
73
74        let xml = serde_xml_rs::to_string(&request)
75            .map_err(|e| Error::generic(format!("Failed to serialize login request: {e}")))?;
76
77        trace!("Login request XML: {}", xml);
78
79        let response = self.client.post_xml("/api/user/login", &xml).await?;
80        let text = response.text().await?;
81
82        trace!("Login response: {}", text);
83
84        let result: Response = serde_xml_rs::from_str(&text)
85            .map_err(|e| Error::generic(format!("Failed to parse login response: {e}")))?;
86
87        if !result.is_success() {
88            let error_code = result.error_code().unwrap_or(-1);
89            let error_message = match error_code {
90                108001 => "Username wrong".to_string(),
91                108002 => "Password wrong".to_string(),
92                108006 => "Username or password wrong".to_string(),
93                108007 => "Too many login attempts".to_string(),
94                _ => result.error_message().unwrap_or("Login failed").to_string(),
95            };
96
97            return Err(Error::api(error_code, error_message));
98        }
99
100        self.client.session().mark_authenticated(username).await;
101
102        debug!("Login successful for user: {}", username);
103        Ok(())
104    }
105
106    /// This endpoint requires authentication and a valid CSRF token.
107    pub async fn logout(&self) -> Result<()> {
108        debug!("Attempting logout");
109
110        let request = LogoutRequest::new();
111        let xml = serde_xml_rs::to_string(&request)
112            .map_err(|e| Error::generic(format!("Failed to serialize logout request: {e}")))?;
113
114        let response = self.client.post_xml("/api/user/logout", &xml).await?;
115        let text = response.text().await?;
116
117        trace!("Logout response: {}", text);
118
119        let result: Response = serde_xml_rs::from_str(&text)
120            .map_err(|e| Error::generic(format!("Failed to parse logout response: {e}")))?;
121
122        if !result.is_success() {
123            return Err(Error::api(
124                result.error_code().unwrap_or(-1),
125                result
126                    .error_message()
127                    .unwrap_or("Logout failed")
128                    .to_string(),
129            ));
130        }
131
132        self.client.session().clear_session().await;
133
134        debug!("Logout successful");
135        Ok(())
136    }
137}
138
139#[cfg(test)]
140mod tests {
141    use super::*;
142    use crate::config::Config;
143
144    #[test]
145    fn test_auth_api_creation() {
146        let config = Config::default();
147        let client = crate::Client::new(config).unwrap();
148        let auth_api = client.auth();
149
150        assert_eq!(
151            std::mem::size_of_val(&auth_api),
152            std::mem::size_of::<&Client>()
153        );
154    }
155
156    #[test]
157    fn test_login_request_serialization() {
158        let request = LoginRequest::new(
159            "admin".to_string(),
160            "encoded_password".to_string(),
161            "4".to_string(),
162        );
163
164        let xml = serde_xml_rs::to_string(&request).unwrap();
165        assert!(xml.contains("<Username>admin</Username>"));
166        assert!(xml.contains("<Password>encoded_password</Password>"));
167        assert!(xml.contains("<password_type>4</password_type>"));
168    }
169
170    #[test]
171    fn test_logout_request_serialization() {
172        let request = LogoutRequest::new();
173        let xml = serde_xml_rs::to_string(&request).unwrap();
174        assert!(xml.contains("<Logout>1</Logout>"));
175    }
176}