Skip to main content

unifi/
config.rs

1use std::fmt;
2use std::time::Duration;
3
4use serde::{Deserialize, Serialize};
5
6/// Default [`UnifiConfig::request_timeout`] when not otherwise specified.
7pub const DEFAULT_REQUEST_TIMEOUT: Duration = Duration::from_secs(30);
8
9/// UniFi controller connection config.
10///
11/// `Debug` redacts [`api_key`](Self::api_key) so this can't leak into logs or
12/// traces through an incidental `{:?}` — only its length is shown.
13#[derive(Clone, Serialize, Deserialize)]
14#[serde(default)]
15pub struct UnifiConfig {
16    /// Controller base URL, e.g. `https://unifi.local` (`UNIFI_URL`).
17    pub url: String,
18    /// API key for the `X-API-KEY` header (`UNIFI_API_KEY`).
19    pub api_key: String,
20    /// Site name (`UNIFI_SITE`, default `"default"`).
21    pub site: String,
22    /// Skip TLS certificate verification. Defaults to `false` (verify) —
23    /// self-signed local UniFi controllers need this explicitly set to
24    /// `true`; a client should never silently accept invalid certificates.
25    pub skip_tls_verify: bool,
26    /// Legacy controller mode: no `/proxy/network` prefix, typically port 8443.
27    pub legacy: bool,
28    /// Per-request timeout, applied to the pooled `reqwest::Client` at
29    /// construction. Defaults to [`DEFAULT_REQUEST_TIMEOUT`]; override for
30    /// controllers or actions (large exports, slow WAN links) that
31    /// routinely need longer than 30s.
32    #[serde(with = "duration_secs")]
33    pub request_timeout: Duration,
34}
35
36impl Default for UnifiConfig {
37    fn default() -> Self {
38        Self {
39            url: String::new(),
40            api_key: String::new(),
41            site: "default".to_string(),
42            skip_tls_verify: false,
43            legacy: false,
44            request_timeout: DEFAULT_REQUEST_TIMEOUT,
45        }
46    }
47}
48
49impl fmt::Debug for UnifiConfig {
50    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
51        f.debug_struct("UnifiConfig")
52            .field("url", &self.url)
53            .field(
54                "api_key",
55                &format_args!("<redacted, {} bytes>", self.api_key.len()),
56            )
57            .field("site", &self.site)
58            .field("skip_tls_verify", &self.skip_tls_verify)
59            .field("legacy", &self.legacy)
60            .field("request_timeout", &self.request_timeout)
61            .finish()
62    }
63}
64
65/// (De)serializes a [`Duration`] as whole seconds. `std::time::Duration`
66/// has no built-in `serde` support, and pulling in `serde_with` or
67/// `humantime-serde` for one field would be a heavier dependency than this
68/// crate's single duration field justifies.
69mod duration_secs {
70    use std::time::Duration;
71
72    use serde::{Deserialize, Deserializer, Serializer};
73
74    pub fn serialize<S: Serializer>(value: &Duration, serializer: S) -> Result<S::Ok, S::Error> {
75        serializer.serialize_u64(value.as_secs())
76    }
77
78    pub fn deserialize<'de, D: Deserializer<'de>>(deserializer: D) -> Result<Duration, D::Error> {
79        Ok(Duration::from_secs(u64::deserialize(deserializer)?))
80    }
81}
82
83#[cfg(test)]
84mod tests {
85    use super::*;
86
87    #[test]
88    fn debug_redacts_api_key() {
89        let cfg = UnifiConfig {
90            api_key: "super-secret-key".to_string(),
91            ..UnifiConfig::default()
92        };
93
94        let debug = format!("{cfg:?}");
95
96        assert!(!debug.contains("super-secret-key"));
97        assert!(debug.contains("redacted"));
98    }
99
100    #[test]
101    fn request_timeout_serializes_as_whole_seconds() {
102        let cfg = UnifiConfig {
103            request_timeout: Duration::from_secs(90),
104            ..UnifiConfig::default()
105        };
106
107        let json = serde_json::to_value(&cfg).unwrap();
108
109        assert_eq!(json["request_timeout"], serde_json::json!(90));
110    }
111
112    #[test]
113    fn request_timeout_round_trips_through_json() {
114        let cfg = UnifiConfig {
115            request_timeout: Duration::from_secs(90),
116            ..UnifiConfig::default()
117        };
118
119        let json = serde_json::to_string(&cfg).unwrap();
120        let restored: UnifiConfig = serde_json::from_str(&json).unwrap();
121
122        assert_eq!(restored.request_timeout, Duration::from_secs(90));
123    }
124}