Skip to main content

unifly_api/convert/
client.rs

1use chrono::Utc;
2
3use crate::integration_types;
4use crate::model::client::{Client, ClientType, GuestAuth, WirelessInfo};
5use crate::model::common::DataSource;
6use crate::model::entity_id::{EntityId, MacAddress};
7use crate::session::models::SessionClientEntry;
8
9use super::helpers::{parse_ip, parse_iso};
10
11// ── Session API ──────────────────────────────────────────────────
12
13fn channel_to_frequency(channel: Option<i32>) -> Option<f32> {
14    channel.map(|ch| match ch {
15        1..=14 => 2.4,
16        32..=68 | 96..=177 => 5.0,
17        _ => 6.0,
18    })
19}
20
21impl From<SessionClientEntry> for Client {
22    fn from(c: SessionClientEntry) -> Self {
23        let is_wired = c.is_wired.unwrap_or(false);
24        let client_type = if is_wired {
25            ClientType::Wired
26        } else {
27            ClientType::Wireless
28        };
29
30        let wireless = if is_wired {
31            None
32        } else {
33            Some(WirelessInfo {
34                ssid: c.essid.clone(),
35                bssid: c.bssid.as_deref().map(MacAddress::new),
36                channel: c.channel.and_then(|ch| ch.try_into().ok()),
37                frequency_ghz: channel_to_frequency(c.channel),
38                signal_dbm: c.signal.or(c.rssi),
39                noise_dbm: c.noise,
40                satisfaction: c.satisfaction.and_then(|s| s.try_into().ok()),
41                tx_rate_kbps: c.tx_rate.and_then(|r| r.try_into().ok()),
42                rx_rate_kbps: c.rx_rate.and_then(|r| r.try_into().ok()),
43            })
44        };
45
46        let is_guest = c.is_guest.unwrap_or(false);
47        let guest_auth = if is_guest {
48            Some(GuestAuth {
49                authorized: c.authorized.unwrap_or(false),
50                method: None,
51                expires_at: None,
52                tx_bytes: c.tx_bytes.and_then(|b| b.try_into().ok()),
53                rx_bytes: c.rx_bytes.and_then(|b| b.try_into().ok()),
54                elapsed_minutes: None,
55            })
56        } else {
57            None
58        };
59
60        let uplink_device_mac = if is_wired {
61            c.sw_mac.as_deref().map(MacAddress::new)
62        } else {
63            c.ap_mac.as_deref().map(MacAddress::new)
64        };
65
66        let switch_port = if is_wired {
67            c.sw_port.and_then(|port| u32::try_from(port).ok())
68        } else {
69            None
70        };
71
72        let connected_at = c.uptime.and_then(|secs| {
73            let duration = chrono::Duration::seconds(secs);
74            Utc::now().checked_sub_signed(duration)
75        });
76
77        Client {
78            id: EntityId::from(c.id),
79            mac: MacAddress::new(&c.mac),
80            ip: parse_ip(c.ip.as_ref()),
81            name: c.name,
82            hostname: c.hostname,
83            client_type,
84            connected_at,
85            uplink_device_id: None,
86            uplink_device_mac,
87            switch_port,
88            network_id: c.network_id.map(EntityId::from),
89            vlan: None,
90            wireless,
91            guest_auth,
92            is_guest,
93            tx_bytes: c.tx_bytes.and_then(|b| b.try_into().ok()),
94            rx_bytes: c.rx_bytes.and_then(|b| b.try_into().ok()),
95            bandwidth: None,
96            os_name: None,
97            device_class: None,
98            use_fixedip: false,
99            fixed_ip: None,
100            blocked: c.blocked.unwrap_or(false),
101            source: DataSource::SessionApi,
102            updated_at: Utc::now(),
103        }
104    }
105}
106
107// ── Integration API ──────────────────────────────────────────────
108
109impl From<integration_types::ClientResponse> for Client {
110    fn from(c: integration_types::ClientResponse) -> Self {
111        let client_type = match c.client_type.as_str() {
112            "WIRED" => ClientType::Wired,
113            "WIRELESS" => ClientType::Wireless,
114            "VPN" => ClientType::Vpn,
115            "TELEPORT" => ClientType::Teleport,
116            _ => ClientType::Unknown,
117        };
118
119        let uuid_fallback = c.id.to_string();
120        let mac_str = c
121            .mac_address
122            .as_deref()
123            .filter(|s| !s.is_empty())
124            .unwrap_or(&uuid_fallback);
125
126        Client {
127            id: EntityId::Uuid(c.id),
128            mac: MacAddress::new(mac_str),
129            ip: c.ip_address.as_deref().and_then(|s| s.parse().ok()),
130            name: Some(c.name),
131            hostname: None,
132            client_type,
133            connected_at: c.connected_at.as_deref().and_then(parse_iso),
134            uplink_device_id: None,
135            uplink_device_mac: None,
136            switch_port: None,
137            network_id: None,
138            vlan: None,
139            wireless: None,
140            guest_auth: None,
141            is_guest: false,
142            tx_bytes: None,
143            rx_bytes: None,
144            bandwidth: None,
145            os_name: None,
146            device_class: None,
147            use_fixedip: false,
148            fixed_ip: None,
149            blocked: false,
150            source: DataSource::IntegrationApi,
151            updated_at: Utc::now(),
152        }
153    }
154}
155
156#[cfg(test)]
157mod tests {
158    use super::*;
159
160    #[test]
161    fn channel_frequency_bands() {
162        assert_eq!(channel_to_frequency(Some(6)), Some(2.4));
163        assert_eq!(channel_to_frequency(Some(36)), Some(5.0));
164        assert_eq!(channel_to_frequency(Some(149)), Some(5.0));
165        assert_eq!(channel_to_frequency(None), None);
166    }
167
168    #[test]
169    fn wired_session_client_carries_switch_port() {
170        let entry: SessionClientEntry = serde_json::from_value(serde_json::json!({
171            "_id": "abc",
172            "mac": "aa:bb:cc:dd:ee:ff",
173            "is_wired": true,
174            "sw_mac": "11:22:33:44:55:66",
175            "sw_port": 9
176        }))
177        .expect("deserialize");
178        let client: Client = entry.into();
179        assert_eq!(client.switch_port, Some(9));
180        assert_eq!(client.client_type, ClientType::Wired);
181    }
182
183    #[test]
184    fn wireless_session_client_drops_switch_port() {
185        let entry: SessionClientEntry = serde_json::from_value(serde_json::json!({
186            "_id": "abc",
187            "mac": "aa:bb:cc:dd:ee:ff",
188            "is_wired": false,
189            "sw_port": 9
190        }))
191        .expect("deserialize");
192        let client: Client = entry.into();
193        assert!(client.switch_port.is_none());
194    }
195}