unifly_api/auth.rs
1use std::sync::Arc;
2
3use reqwest::cookie::Jar;
4use secrecy::SecretString;
5
6/// Which authentication strategy to use for a particular API call.
7///
8/// Marker enum (no data) -- the actual credentials live in [`Credentials`].
9/// Useful for branching on auth flow without carrying secret material.
10#[derive(Debug, Clone, Copy, PartialEq, Eq)]
11pub enum AuthStrategy {
12 /// Cookie-based session (session API, WebSocket).
13 Session,
14 /// Local API key header (Integration API).
15 ApiKey,
16 /// Cloud API key (Site Manager, Cloud Connector).
17 CloudApiKey,
18}
19
20/// Credentials for authenticating with a UniFi controller.
21///
22/// Each variant carries the secret material needed for its auth flow.
23#[derive(Debug, Clone)]
24pub enum Credentials {
25 /// Cookie-based session auth. The jar holds the session cookie
26 /// after a successful login; pass it into the `reqwest::Client` builder.
27 Session { cookie_jar: Arc<Jar> },
28
29 /// Local API key for the Integration API.
30 /// Generated at: Network > Settings > Control Plane > Integrations.
31 ApiKey { key: SecretString },
32
33 /// Cloud API key for Site Manager + Cloud Connector.
34 /// Generated at: <https://unifi.ui.com> > Settings > API Keys.
35 Cloud { key: SecretString, host_id: String },
36}
37
38/// The platform type of the UniFi controller.
39///
40/// Determines URL prefixes, login paths, and which API surfaces are available.
41#[derive(Debug, Clone, Copy, PartialEq, Eq)]
42pub enum ControllerPlatform {
43 /// UniFi OS device (UDM, UCG, etc.) -- port 443, `/proxy/network/` prefix.
44 UnifiOs,
45 /// Standalone Network Application (Java) -- port 8443, no prefix.
46 ClassicController,
47 /// Cloud-hosted via Site Manager / Cloud Connector (api.ui.com).
48 Cloud,
49}
50
51impl ControllerPlatform {
52 /// The path prefix for session API endpoints.
53 ///
54 /// Returns `None` for [`Cloud`](Self::Cloud) because the session API
55 /// is not available via the cloud connector.
56 pub fn session_prefix(&self) -> Option<&'static str> {
57 match self {
58 Self::UnifiOs => Some("/proxy/network"),
59 Self::ClassicController => Some(""),
60 Self::Cloud => None,
61 }
62 }
63
64 /// The path prefix for the Integration API.
65 ///
66 /// On UniFi OS devices: `/proxy/network/integration`
67 /// On standalone controllers: `/integration`
68 /// On cloud connector: `/proxy/network/integration`
69 pub fn integration_prefix(&self) -> &'static str {
70 match self {
71 Self::UnifiOs | Self::Cloud => "/proxy/network/integration",
72 Self::ClassicController => "/integration",
73 }
74 }
75
76 /// The login endpoint path.
77 ///
78 /// Returns `None` for [`Cloud`](Self::Cloud) because cloud uses
79 /// API key auth -- no session login needed.
80 pub fn login_path(&self) -> Option<&'static str> {
81 match self {
82 Self::UnifiOs => Some("/api/auth/login"),
83 Self::ClassicController => Some("/api/login"),
84 Self::Cloud => None,
85 }
86 }
87
88 /// The logout endpoint path.
89 ///
90 /// Returns `None` for [`Cloud`](Self::Cloud).
91 pub fn logout_path(&self) -> Option<&'static str> {
92 match self {
93 Self::UnifiOs => Some("/api/auth/logout"),
94 Self::ClassicController => Some("/api/logout"),
95 Self::Cloud => None,
96 }
97 }
98
99 /// The WebSocket path template. `{site}` must be replaced by the caller.
100 ///
101 /// Returns `None` for [`Cloud`](Self::Cloud) because WebSocket
102 /// connections are not available via the cloud connector.
103 pub fn websocket_path(&self) -> Option<&'static str> {
104 match self {
105 Self::UnifiOs => Some("/proxy/network/wss/s/{site}/events"),
106 Self::ClassicController => Some("/wss/s/{site}/events"),
107 Self::Cloud => None,
108 }
109 }
110}
111
112#[cfg(test)]
113mod tests {
114 use super::ControllerPlatform;
115
116 #[test]
117 fn cloud_uses_unifi_os_integration_prefix() {
118 assert_eq!(
119 ControllerPlatform::Cloud.integration_prefix(),
120 "/proxy/network/integration"
121 );
122 }
123}