Skip to main content

unifly_api/legacy/
auth.rs

1// Legacy API authentication
2//
3// Cookie-based session login/logout and controller platform detection.
4// The login endpoint sets a session cookie in the client's jar;
5// subsequent requests use that cookie automatically.
6
7use secrecy::{ExposeSecret, SecretString};
8use serde_json::json;
9use tracing::debug;
10use url::Url;
11
12use crate::auth::ControllerPlatform;
13use crate::error::Error;
14use crate::legacy::client::LegacyClient;
15
16impl LegacyClient {
17    /// Authenticate with the controller using username/password.
18    ///
19    /// On success the session cookie is stored in the client's cookie jar
20    /// and used for all subsequent requests. The login endpoint differs
21    /// by platform:
22    /// - UniFi OS: `POST /api/auth/login`
23    /// - Standalone: `POST /api/login`
24    pub async fn login(&self, username: &str, password: &SecretString) -> Result<(), Error> {
25        let login_path = self
26            .platform()
27            .login_path()
28            .ok_or_else(|| Error::Authentication {
29                message: "login not supported on cloud platform".into(),
30            })?;
31
32        let url = self
33            .base_url()
34            .join(login_path)
35            .map_err(Error::InvalidUrl)?;
36
37        debug!("logging in at {}", url);
38
39        let body = json!({
40            "username": username,
41            "password": password.expose_secret(),
42        });
43
44        let resp = self
45            .http()
46            .post(url)
47            .json(&body)
48            .send()
49            .await
50            .map_err(Error::Transport)?;
51
52        let status = resp.status();
53        if !status.is_success() {
54            let body = resp.text().await.unwrap_or_default();
55            return Err(Error::Authentication {
56                message: format!("login failed (HTTP {status}): {body}"),
57            });
58        }
59
60        // Capture CSRF token from login response — required for all
61        // POST/PUT/DELETE requests through the UniFi OS proxy.
62        if let Some(token) = resp
63            .headers()
64            .get("X-CSRF-Token")
65            .or_else(|| resp.headers().get("x-csrf-token"))
66            .and_then(|v| v.to_str().ok())
67        {
68            self.set_csrf_token(token.to_owned());
69        }
70
71        debug!("login successful");
72        Ok(())
73    }
74
75    /// End the current session.
76    ///
77    /// Platform-specific logout endpoint:
78    /// - UniFi OS: `POST /api/auth/logout`
79    /// - Standalone: `POST /api/logout`
80    pub async fn logout(&self) -> Result<(), Error> {
81        let logout_path = self
82            .platform()
83            .logout_path()
84            .ok_or_else(|| Error::Authentication {
85                message: "logout not supported on cloud platform".into(),
86            })?;
87
88        let url = self
89            .base_url()
90            .join(logout_path)
91            .map_err(Error::InvalidUrl)?;
92
93        debug!("logging out at {}", url);
94
95        let _resp = self
96            .http()
97            .post(url)
98            .send()
99            .await
100            .map_err(Error::Transport)?;
101
102        debug!("logout complete");
103        Ok(())
104    }
105
106    /// Auto-detect the controller platform by probing login endpoints.
107    ///
108    /// Tries the UniFi OS endpoint first (`/api/auth/login`). If it
109    /// responds (even with an error), we're on UniFi OS. If the connection
110    /// fails or returns 404, falls back to standalone detection.
111    pub async fn detect_platform(base_url: &Url) -> Result<ControllerPlatform, Error> {
112        let http = reqwest::Client::builder()
113            .danger_accept_invalid_certs(true)
114            .build()
115            .map_err(Error::Transport)?;
116
117        // Probe UniFi OS endpoint
118        let unifi_os_url = base_url
119            .join("/api/auth/login")
120            .map_err(Error::InvalidUrl)?;
121
122        debug!("probing UniFi OS at {}", unifi_os_url);
123
124        if let Ok(resp) = http.get(unifi_os_url).send().await {
125            // UniFi OS returns a response (even 401/405) at this path.
126            // Standalone controllers don't have this endpoint at all.
127            if resp.status() != reqwest::StatusCode::NOT_FOUND {
128                debug!("detected UniFi OS platform");
129                return Ok(ControllerPlatform::UnifiOs);
130            }
131        }
132        // Connection error -- might be standalone on a different port
133
134        // Probe standalone endpoint
135        let standalone_url = base_url.join("/api/login").map_err(Error::InvalidUrl)?;
136
137        debug!("probing standalone at {}", standalone_url);
138
139        match http.get(standalone_url).send().await {
140            Ok(_) => {
141                debug!("detected standalone (classic) controller");
142                Ok(ControllerPlatform::ClassicController)
143            }
144            Err(e) => Err(Error::Transport(e)),
145        }
146    }
147}