diff --git a/backend/src/db/devices.rs b/backend/src/db/devices.rs index 6d9cda6..711ea4b 100644 --- a/backend/src/db/devices.rs +++ b/backend/src/db/devices.rs @@ -183,18 +183,18 @@ pub fn get_summary() -> Result { }) } +// Records a sighting of an existing device. Registration fields (is_registered, owner) are +// owned by register/unregister and are intentionally never touched here. pub fn update(device: Device) -> Result<(), DbError> { let conn = db::get_db_connection(); - let mut sql = "UPDATE devices SET ipv4_address=?, last_seen=?, is_registered=?, owner=?".to_string(); + let mut sql = "UPDATE devices SET ipv4_address=?, last_seen=?".to_string(); let mut params: Vec = vec![ device.ipv4_address.clone().into(), device .last_seen .to_rfc3339_opts(chrono::SecondsFormat::Nanos, false) .into(), - device.is_registered.into(), - device.owner.clone().into(), ]; // Only write the vendor (and its derived device_type) when deduced, so a sighting that @@ -227,6 +227,42 @@ pub fn update(device: Device) -> Result<(), DbError> { } } +pub fn register(mac_address: String, owner: String, device_type: String) -> Result<(), DbError> { + let conn = db::get_db_connection(); + + match conn.execute( + "UPDATE devices SET is_registered=1, owner=?1, device_type=?2 WHERE mac_address=?3", + params![owner, device_type, mac_address], + ) { + Ok(_) => { + debug!("Device registered in database: {mac_address}"); + Ok(()) + } + Err(error) => { + error!("Error registering device ({mac_address}) in database: {error}"); + Err(DbError::from(error)) + } + } +} + +pub fn unregister(mac_address: String) -> Result<(), DbError> { + let conn = db::get_db_connection(); + + match conn.execute( + "UPDATE devices SET is_registered=0, owner='' WHERE mac_address=?1", + params![mac_address], + ) { + Ok(_) => { + debug!("Device unregistered in database: {mac_address}"); + Ok(()) + } + Err(error) => { + error!("Error unregistering device ({mac_address}) in database: {error}"); + Err(DbError::from(error)) + } + } +} + #[cfg(test)] mod tests { use chrono::{DateTime, TimeZone, Utc}; @@ -392,10 +428,22 @@ mod tests { )) .unwrap(); + // Register the device, then update it with a Device carrying the scanner defaults + // (is_registered=false, owner=""). The update must NOT clobber the registration while + // still refreshing sighting fields like ipv4_address and last_seen. + register( + "uu:tt:tt:tt:tt:aa".to_string(), + "Grace".to_string(), + "Phone".to_string(), + ) + .unwrap(); + + let new_last_seen = Utc::now(); let mut device = read("uu:tt:tt:tt:tt:aa".to_string()).unwrap(); - device.is_registered = true; - device.owner = "Grace".to_string(); - device.device_type = "Phone".to_string(); + device.is_registered = false; + device.owner = "".to_string(); + device.ipv4_address = "192.168.200.50".to_string(); + device.last_seen = new_last_seen; update(device).unwrap(); @@ -405,9 +453,9 @@ mod tests { device, "uu:tt:tt:tt:tt:aa".to_string(), "Phone".to_string(), - "192.168.200.1".to_string(), + "192.168.200.50".to_string(), true, - last_seen, + new_last_seen, "Grace".to_string(), "Test vendor".to_string(), ); @@ -521,6 +569,63 @@ mod tests { assert_eq!(device.device_type, "tablet".to_string()); } + #[tokio::test] + async fn test_register() { + tests_common::setup().await; + + // A device with no deduced vendor must still persist the device_type chosen at registration. + let last_seen = Utc::now(); + insert(Device::new( + "rr:rr:rr:rr:rr:01".to_string(), + "192.168.230.1".to_string(), + "".to_string(), + last_seen, + )) + .unwrap(); + + register( + "rr:rr:rr:rr:rr:01".to_string(), + "Grace".to_string(), + "Phone".to_string(), + ) + .unwrap(); + + let device = read("rr:rr:rr:rr:rr:01".to_string()).unwrap(); + assert!(device.is_registered, "Device should be registered"); + assert_eq!(device.owner, "Grace".to_string()); + assert_eq!(device.device_type, "Phone".to_string()); + assert_eq!(device.vendor, "".to_string()); + } + + #[tokio::test] + async fn test_unregister() { + tests_common::setup().await; + + let last_seen = Utc::now(); + insert(Device::new( + "rr:rr:rr:rr:rr:02".to_string(), + "192.168.230.2".to_string(), + "Test vendor".to_string(), + last_seen, + )) + .unwrap(); + + register( + "rr:rr:rr:rr:rr:02".to_string(), + "Grace".to_string(), + "Phone".to_string(), + ) + .unwrap(); + + unregister("rr:rr:rr:rr:rr:02".to_string()).unwrap(); + + let device = read("rr:rr:rr:rr:rr:02".to_string()).unwrap(); + assert!(!device.is_registered, "Device should not be registered"); + assert_eq!(device.owner, "".to_string()); + // device_type is left untouched by unregister + assert_eq!(device.device_type, "Phone".to_string()); + } + #[tokio::test] async fn test_insert() { tests_common::setup().await; diff --git a/backend/src/mdns_scanner.rs b/backend/src/mdns_scanner.rs index 27c8c37..2df2a88 100644 --- a/backend/src/mdns_scanner.rs +++ b/backend/src/mdns_scanner.rs @@ -88,6 +88,11 @@ async fn process_announcement( match db::devices::read(mac.clone()) { Some(recorded) => { debug!("mDNS sighting of known device {mac}; updating"); + // Keep the previously stored hostname rather than overwriting it with this + // announcement's hostname. + if recorded.name.is_some() { + device.name = recorded.name.clone(); + } if let Err(err) = db::devices::update(device.clone()) { error!("Failed to update mDNS device {mac}: {err}"); return; diff --git a/backend/src/web_server/devices.rs b/backend/src/web_server/devices.rs index 712f975..c35f329 100644 --- a/backend/src/web_server/devices.rs +++ b/backend/src/web_server/devices.rs @@ -105,7 +105,7 @@ pub async fn register(Json(payload): Json) -> impl IntoRe payload.mac_address, payload.owner, payload.device_type ); - let mut device = match db::devices::read(payload.mac_address) { + let device = match db::devices::read(payload.mac_address.clone()) { Some(value) => value, None => { return ( @@ -122,11 +122,7 @@ pub async fn register(Json(payload): Json) -> impl IntoRe ); } - device.is_registered = true; - device.owner = payload.owner; - device.device_type = payload.device_type; - - match db::devices::update(device) { + match db::devices::register(payload.mac_address, payload.owner, payload.device_type) { Ok(_) => (axum::http::StatusCode::CREATED, "Device registered"), Err(err) => { error!("Error registering device in the database: {}", err); @@ -154,7 +150,7 @@ pub async fn register(Json(payload): Json) -> impl IntoRe security(("bearer_auth" = [])) )] pub async fn unregister(Path(mac_address): Path) -> impl IntoResponse { - let mut device = match db::devices::read(mac_address) { + let device = match db::devices::read(mac_address.clone()) { Some(value) => value, None => { return ( @@ -171,10 +167,7 @@ pub async fn unregister(Path(mac_address): Path) -> impl IntoResponse { ); } - device.is_registered = false; - device.owner = "".to_string(); - - match db::devices::update(device) { + match db::devices::unregister(mac_address) { Ok(_) => (axum::http::StatusCode::OK, "Device un-registered"), Err(err) => { error!("Error updating device in the database: {}", err);