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
11fn 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
107impl 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}