mirror of
https://github.com/rzuasti/oott.git
synced 2026-07-08 19:21:54 +02:00
Split db::devices update into seen/update and expose PUT endpoint
Scanners now call seen() (sighting semantics: stamp last_seen, preserve
registration, conditional vendor/device_type/name writes). update() is
the user-editable mutation (owner, device_type, vendor) wired to
PUT /api/devices/{mac_address}.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.7
parent
792a309136
commit
54211c57d7
@@ -10,7 +10,7 @@
|
|||||||
- [x] One event for each device appearance
|
- [x] One event for each device appearance
|
||||||
- [x] Add date filter to device event list method (from)
|
- [x] Add date filter to device event list method (from)
|
||||||
- [x] Add a data set (json) to store maps from vendors -> device type; modify the device creation so that it uses it automatically
|
- [x] Add a data set (json) to store maps from vendors -> device type; modify the device creation so that it uses it automatically
|
||||||
- [ ] Separate the devices "update" method into update and seen (one for API edit one for scanners)
|
- [x] Separate the devices "update" method into update and seen (one for API edit one for scanners)
|
||||||
|
|
||||||
## Frontend
|
## Frontend
|
||||||
|
|
||||||
|
|||||||
@@ -32,7 +32,13 @@ pub async fn scan() -> Result<(), Box<dyn std::error::Error>> {
|
|||||||
"Device found in database {}. Updating to {}.",
|
"Device found in database {}. Updating to {}.",
|
||||||
recorded_device, device
|
recorded_device, device
|
||||||
);
|
);
|
||||||
db::devices::update(device.clone())?;
|
db::devices::seen(
|
||||||
|
device.mac_address.clone(),
|
||||||
|
device.ipv4_address.clone(),
|
||||||
|
device.vendor.clone(),
|
||||||
|
device.device_type.clone(),
|
||||||
|
device.name.clone(),
|
||||||
|
)?;
|
||||||
events::trigger_existing_device(recorded_device, device.clone()).ok(); // Ignoring errors here, do not stop loop if notification delivery fails
|
events::trigger_existing_device(recorded_device, device.clone()).ok(); // Ignoring errors here, do not stop loop if notification delivery fails
|
||||||
}
|
}
|
||||||
None => {
|
None => {
|
||||||
|
|||||||
+173
-87
@@ -183,16 +183,22 @@ pub fn get_summary() -> Result<DeviceSummary, DbError> {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
// Records a sighting of an existing device. Registration fields (is_registered, owner) are
|
// Records a sighting of an existing device by a scanner. last_seen is stamped here with the
|
||||||
// owned by register/unregister and are intentionally never touched here.
|
// current time. Registration fields (is_registered, owner) are owned by register/unregister
|
||||||
pub fn update(device: Device) -> Result<(), DbError> {
|
// and are intentionally never touched here.
|
||||||
|
pub fn seen(
|
||||||
|
mac_address: String,
|
||||||
|
ipv4_address: String,
|
||||||
|
vendor: String,
|
||||||
|
device_type: String,
|
||||||
|
name: Option<String>,
|
||||||
|
) -> Result<(), DbError> {
|
||||||
let conn = db::get_db_connection();
|
let conn = db::get_db_connection();
|
||||||
|
|
||||||
let mut sql = "UPDATE devices SET ipv4_address=?, last_seen=?".to_string();
|
let mut sql = "UPDATE devices SET ipv4_address=?, last_seen=?".to_string();
|
||||||
let mut params: Vec<rusqlite::types::Value> = vec![
|
let mut params: Vec<rusqlite::types::Value> = vec![
|
||||||
device.ipv4_address.clone().into(),
|
ipv4_address.into(),
|
||||||
device
|
Utc::now()
|
||||||
.last_seen
|
|
||||||
.to_rfc3339_opts(chrono::SecondsFormat::Nanos, false)
|
.to_rfc3339_opts(chrono::SecondsFormat::Nanos, false)
|
||||||
.into(),
|
.into(),
|
||||||
];
|
];
|
||||||
@@ -201,29 +207,54 @@ pub fn update(device: Device) -> Result<(), DbError> {
|
|||||||
// could not determine a vendor (empty string) never clobbers a previously known one.
|
// could not determine a vendor (empty string) never clobbers a previously known one.
|
||||||
// device_type is only written when the stored value is empty, so a value chosen by the
|
// device_type is only written when the stored value is empty, so a value chosen by the
|
||||||
// user (via register) or previously deduced is never overwritten by a later sighting.
|
// user (via register) or previously deduced is never overwritten by a later sighting.
|
||||||
if !device.vendor.is_empty() {
|
if !vendor.is_empty() {
|
||||||
sql.push_str(", vendor=?, device_type=CASE WHEN device_type='' THEN ? ELSE device_type END");
|
sql.push_str(", vendor=?, device_type=CASE WHEN device_type='' THEN ? ELSE device_type END");
|
||||||
params.push(device.vendor.clone().into());
|
params.push(vendor.into());
|
||||||
params.push(device.device_type.clone().into());
|
params.push(device_type.into());
|
||||||
}
|
}
|
||||||
|
|
||||||
// Only write the name column when it is set, so an ARP rescan (name: None) never
|
// Only write the name column when it is set, so an ARP rescan (name: None) never
|
||||||
// clobbers a hostname previously stored by the mDNS scanner.
|
// clobbers a hostname previously stored by the mDNS scanner.
|
||||||
if let Some(name) = &device.name {
|
if let Some(name) = name {
|
||||||
sql.push_str(", name=?");
|
sql.push_str(", name=?");
|
||||||
params.push(name.clone().into());
|
params.push(name.into());
|
||||||
}
|
}
|
||||||
|
|
||||||
sql.push_str(" WHERE mac_address=?");
|
sql.push_str(" WHERE mac_address=?");
|
||||||
params.push(device.mac_address.clone().into());
|
params.push(mac_address.clone().into());
|
||||||
|
|
||||||
match conn.execute(sql.as_str(), params_from_iter(params.iter())) {
|
match conn.execute(sql.as_str(), params_from_iter(params.iter())) {
|
||||||
Ok(_) => {
|
Ok(_) => {
|
||||||
debug!("Device updated in database: {}", device);
|
debug!("Device sighting recorded in database: {mac_address}");
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
Err(error) => {
|
Err(error) => {
|
||||||
error!("Error updating device ({device}) in database: {error}");
|
error!("Error recording device sighting ({mac_address}) in database: {error}");
|
||||||
|
Err(DbError::from(error))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Modifies user-editable fields of a device. Intended for the UI's "modify" feature; scanners
|
||||||
|
// must use seen() instead.
|
||||||
|
pub fn update(
|
||||||
|
mac_address: String,
|
||||||
|
owner: String,
|
||||||
|
device_type: String,
|
||||||
|
vendor: String,
|
||||||
|
) -> Result<(), DbError> {
|
||||||
|
let conn = db::get_db_connection();
|
||||||
|
|
||||||
|
match conn.execute(
|
||||||
|
"UPDATE devices SET owner=?1, device_type=?2, vendor=?3 WHERE mac_address=?4",
|
||||||
|
params![owner, device_type, vendor, mac_address],
|
||||||
|
) {
|
||||||
|
Ok(_) => {
|
||||||
|
debug!("Device updated in database: {mac_address}");
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
Err(error) => {
|
||||||
|
error!("Error updating device ({mac_address}) in database: {error}");
|
||||||
Err(DbError::from(error))
|
Err(DbError::from(error))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -416,23 +447,22 @@ mod tests {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn test_update() {
|
async fn test_seen() {
|
||||||
tests_common::setup().await;
|
tests_common::setup().await;
|
||||||
|
|
||||||
// Insert a device, update it and validate it
|
// Insert a device, record a sighting and validate it
|
||||||
|
|
||||||
let last_seen = Utc::now();
|
let initial_last_seen = Utc.with_ymd_and_hms(2020, 1, 1, 0, 0, 0).unwrap();
|
||||||
insert(Device::new(
|
insert(Device::new(
|
||||||
"uu:tt:tt:tt:tt:aa".to_string(),
|
"uu:tt:tt:tt:tt:aa".to_string(),
|
||||||
"192.168.200.1".to_string(),
|
"192.168.200.1".to_string(),
|
||||||
"Test vendor".to_string(),
|
"Test vendor".to_string(),
|
||||||
last_seen,
|
initial_last_seen,
|
||||||
))
|
))
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
|
||||||
// Register the device, then update it with a Device carrying the scanner defaults
|
// Register the device, then record a sighting. seen() must NOT clobber the
|
||||||
// (is_registered=false, owner=""). The update must NOT clobber the registration while
|
// registration while still refreshing sighting fields (ipv4_address, last_seen).
|
||||||
// still refreshing sighting fields like ipv4_address and last_seen.
|
|
||||||
register(
|
register(
|
||||||
"uu:tt:tt:tt:tt:aa".to_string(),
|
"uu:tt:tt:tt:tt:aa".to_string(),
|
||||||
"Grace".to_string(),
|
"Grace".to_string(),
|
||||||
@@ -440,56 +470,44 @@ mod tests {
|
|||||||
)
|
)
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
|
||||||
let new_last_seen = Utc::now();
|
let before = Utc::now();
|
||||||
let mut device = read("uu:tt:tt:tt:tt:aa".to_string()).unwrap();
|
seen(
|
||||||
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();
|
|
||||||
|
|
||||||
let device = read("uu:tt:tt:tt:tt:aa".to_string()).unwrap();
|
|
||||||
|
|
||||||
validate_device(
|
|
||||||
device,
|
|
||||||
"uu:tt:tt:tt:tt:aa".to_string(),
|
"uu:tt:tt:tt:tt:aa".to_string(),
|
||||||
"Phone".to_string(),
|
|
||||||
"192.168.200.50".to_string(),
|
"192.168.200.50".to_string(),
|
||||||
true,
|
|
||||||
new_last_seen,
|
|
||||||
"Grace".to_string(),
|
|
||||||
"Test vendor".to_string(),
|
"Test vendor".to_string(),
|
||||||
);
|
"".to_string(),
|
||||||
|
None,
|
||||||
// Insert a device, change its MAC address and validate it hasnt changed (it cannot)
|
)
|
||||||
let last_seen = Utc::now();
|
|
||||||
insert(Device::new(
|
|
||||||
"uu:tt:tt:tt:tt:bb".to_string(),
|
|
||||||
"192.168.200.3".to_string(),
|
|
||||||
"Test vendor".to_string(),
|
|
||||||
last_seen,
|
|
||||||
))
|
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
|
||||||
let mut device = read("uu:tt:tt:tt:tt:bb".to_string()).unwrap();
|
let device = read("uu:tt:tt:tt:tt:aa".to_string()).unwrap();
|
||||||
device.mac_address = "uu:tt:tt:tt:tt:cc".to_string();
|
assert_eq!(device.ipv4_address, "192.168.200.50".to_string());
|
||||||
|
assert!(
|
||||||
update(device).unwrap();
|
device.last_seen >= before,
|
||||||
|
"seen() should stamp last_seen with the current time"
|
||||||
|
);
|
||||||
|
assert!(device.is_registered);
|
||||||
|
assert_eq!(device.owner, "Grace".to_string());
|
||||||
|
assert_eq!(device.device_type, "Phone".to_string());
|
||||||
|
assert_eq!(device.vendor, "Test vendor".to_string());
|
||||||
|
|
||||||
|
// A seen() call for a MAC that doesn't exist is a no-op (no row matches)
|
||||||
|
seen(
|
||||||
|
"uu:tt:tt:tt:tt:cc".to_string(),
|
||||||
|
"192.168.200.99".to_string(),
|
||||||
|
"Test vendor".to_string(),
|
||||||
|
"".to_string(),
|
||||||
|
None,
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
assert!(
|
assert!(
|
||||||
read("uu:tt:tt:tt:tt:cc".to_string()).is_none(),
|
read("uu:tt:tt:tt:tt:cc".to_string()).is_none(),
|
||||||
"Device should not exist"
|
"seen() must not insert new devices"
|
||||||
);
|
);
|
||||||
|
|
||||||
assert!(
|
|
||||||
read("uu:tt:tt:tt:tt:bb".to_string()).is_some(),
|
|
||||||
"Device should exist"
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn test_update_name_set_and_preserve() {
|
async fn test_seen_name_set_and_preserve() {
|
||||||
tests_common::setup().await;
|
tests_common::setup().await;
|
||||||
|
|
||||||
let last_seen = Utc::now();
|
let last_seen = Utc::now();
|
||||||
@@ -505,27 +523,41 @@ mod tests {
|
|||||||
assert_eq!(read("nn:nn:nn:nn:nn:01".to_string()).unwrap().name, None);
|
assert_eq!(read("nn:nn:nn:nn:nn:01".to_string()).unwrap().name, None);
|
||||||
|
|
||||||
// mDNS sets the name
|
// mDNS sets the name
|
||||||
let mut device = read("nn:nn:nn:nn:nn:01".to_string()).unwrap();
|
seen(
|
||||||
device.name = Some("host.local".to_string());
|
"nn:nn:nn:nn:nn:01".to_string(),
|
||||||
update(device).unwrap();
|
"192.168.210.1".to_string(),
|
||||||
|
"Test vendor".to_string(),
|
||||||
|
"".to_string(),
|
||||||
|
Some("host.local".to_string()),
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
read("nn:nn:nn:nn:nn:01".to_string()).unwrap().name,
|
read("nn:nn:nn:nn:nn:01".to_string()).unwrap().name,
|
||||||
Some("host.local".to_string())
|
Some("host.local".to_string())
|
||||||
);
|
);
|
||||||
|
|
||||||
// An ARP rescan (name: None) must NOT clobber the stored name
|
// An ARP rescan (name: None) must NOT clobber the stored name
|
||||||
let mut device = read("nn:nn:nn:nn:nn:01".to_string()).unwrap();
|
seen(
|
||||||
device.name = None;
|
"nn:nn:nn:nn:nn:01".to_string(),
|
||||||
device.ipv4_address = "192.168.210.99".to_string();
|
"192.168.210.99".to_string(),
|
||||||
update(device).unwrap();
|
"Test vendor".to_string(),
|
||||||
|
"".to_string(),
|
||||||
|
None,
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
let device = read("nn:nn:nn:nn:nn:01".to_string()).unwrap();
|
let device = read("nn:nn:nn:nn:nn:01".to_string()).unwrap();
|
||||||
assert_eq!(device.name, Some("host.local".to_string()));
|
assert_eq!(device.name, Some("host.local".to_string()));
|
||||||
assert_eq!(device.ipv4_address, "192.168.210.99".to_string());
|
assert_eq!(device.ipv4_address, "192.168.210.99".to_string());
|
||||||
|
|
||||||
// A later mDNS sighting updates the name
|
// A later mDNS sighting updates the name
|
||||||
let mut device = read("nn:nn:nn:nn:nn:01".to_string()).unwrap();
|
seen(
|
||||||
device.name = Some("renamed.local".to_string());
|
"nn:nn:nn:nn:nn:01".to_string(),
|
||||||
update(device).unwrap();
|
"192.168.210.99".to_string(),
|
||||||
|
"Test vendor".to_string(),
|
||||||
|
"".to_string(),
|
||||||
|
Some("renamed.local".to_string()),
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
read("nn:nn:nn:nn:nn:01".to_string()).unwrap().name,
|
read("nn:nn:nn:nn:nn:01".to_string()).unwrap().name,
|
||||||
Some("renamed.local".to_string())
|
Some("renamed.local".to_string())
|
||||||
@@ -533,7 +565,7 @@ mod tests {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn test_update_vendor_set_and_preserve() {
|
async fn test_seen_vendor_set_and_preserve() {
|
||||||
tests_common::setup().await;
|
tests_common::setup().await;
|
||||||
|
|
||||||
let last_seen = Utc::now();
|
let last_seen = Utc::now();
|
||||||
@@ -545,17 +577,25 @@ mod tests {
|
|||||||
))
|
))
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
|
||||||
let mut device = read("vv:vv:vv:vv:vv:01".to_string()).unwrap();
|
seen(
|
||||||
device.device_type = "phone".to_string();
|
"vv:vv:vv:vv:vv:01".to_string(),
|
||||||
update(device).unwrap();
|
"192.168.220.1".to_string(),
|
||||||
|
"Apple, Inc.".to_string(),
|
||||||
|
"phone".to_string(),
|
||||||
|
None,
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
// A re-sighting that could not deduce a vendor (empty) must NOT clobber the known one,
|
// A re-sighting that could not deduce a vendor (empty) must NOT clobber the known one,
|
||||||
// nor its derived device_type, but other fields still update.
|
// nor its derived device_type, but other fields still update.
|
||||||
let mut device = read("vv:vv:vv:vv:vv:01".to_string()).unwrap();
|
seen(
|
||||||
device.vendor = "".to_string();
|
"vv:vv:vv:vv:vv:01".to_string(),
|
||||||
device.device_type = "".to_string();
|
"192.168.220.99".to_string(),
|
||||||
device.ipv4_address = "192.168.220.99".to_string();
|
"".to_string(),
|
||||||
update(device).unwrap();
|
"".to_string(),
|
||||||
|
None,
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
let device = read("vv:vv:vv:vv:vv:01".to_string()).unwrap();
|
let device = read("vv:vv:vv:vv:vv:01".to_string()).unwrap();
|
||||||
assert_eq!(device.vendor, "Apple, Inc.".to_string());
|
assert_eq!(device.vendor, "Apple, Inc.".to_string());
|
||||||
assert_eq!(device.device_type, "phone".to_string());
|
assert_eq!(device.device_type, "phone".to_string());
|
||||||
@@ -563,17 +603,21 @@ mod tests {
|
|||||||
|
|
||||||
// A later sighting that deduces a different vendor updates vendor but must NOT
|
// A later sighting that deduces a different vendor updates vendor but must NOT
|
||||||
// overwrite a device_type that is already set.
|
// overwrite a device_type that is already set.
|
||||||
let mut device = read("vv:vv:vv:vv:vv:01".to_string()).unwrap();
|
seen(
|
||||||
device.vendor = "Google, Inc.".to_string();
|
"vv:vv:vv:vv:vv:01".to_string(),
|
||||||
device.device_type = "tablet".to_string();
|
"192.168.220.99".to_string(),
|
||||||
update(device).unwrap();
|
"Google, Inc.".to_string(),
|
||||||
|
"tablet".to_string(),
|
||||||
|
None,
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
let device = read("vv:vv:vv:vv:vv:01".to_string()).unwrap();
|
let device = read("vv:vv:vv:vv:vv:01".to_string()).unwrap();
|
||||||
assert_eq!(device.vendor, "Google, Inc.".to_string());
|
assert_eq!(device.vendor, "Google, Inc.".to_string());
|
||||||
assert_eq!(device.device_type, "phone".to_string());
|
assert_eq!(device.device_type, "phone".to_string());
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn test_update_preserves_registered_device_type() {
|
async fn test_seen_preserves_registered_device_type() {
|
||||||
tests_common::setup().await;
|
tests_common::setup().await;
|
||||||
|
|
||||||
// A device registered with a user-chosen device_type must not have it overwritten
|
// A device registered with a user-chosen device_type must not have it overwritten
|
||||||
@@ -594,16 +638,58 @@ mod tests {
|
|||||||
)
|
)
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
|
||||||
let mut device = read("pp:pp:pp:pp:pp:01".to_string()).unwrap();
|
seen(
|
||||||
device.vendor = "Apple, Inc.".to_string();
|
"pp:pp:pp:pp:pp:01".to_string(),
|
||||||
device.device_type = "Phone".to_string();
|
"192.168.240.1".to_string(),
|
||||||
update(device).unwrap();
|
"Apple, Inc.".to_string(),
|
||||||
|
"Phone".to_string(),
|
||||||
|
None,
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
let device = read("pp:pp:pp:pp:pp:01".to_string()).unwrap();
|
let device = read("pp:pp:pp:pp:pp:01".to_string()).unwrap();
|
||||||
assert_eq!(device.vendor, "Apple, Inc.".to_string());
|
assert_eq!(device.vendor, "Apple, Inc.".to_string());
|
||||||
assert_eq!(device.device_type, "Laptop".to_string());
|
assert_eq!(device.device_type, "Laptop".to_string());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn test_update() {
|
||||||
|
tests_common::setup().await;
|
||||||
|
|
||||||
|
// update() overwrites owner, device_type and vendor unconditionally, leaving sighting
|
||||||
|
// fields (ipv4_address, last_seen, name) and is_registered untouched.
|
||||||
|
let last_seen = Utc::now();
|
||||||
|
insert(Device {
|
||||||
|
mac_address: "mm:mm:mm:mm:mm:01".to_string(),
|
||||||
|
ipv4_address: "192.168.250.1".to_string(),
|
||||||
|
vendor: "Old Vendor".to_string(),
|
||||||
|
last_seen,
|
||||||
|
is_registered: true,
|
||||||
|
owner: "Alice".to_string(),
|
||||||
|
device_type: "Phone".to_string(),
|
||||||
|
name: Some("host.local".to_string()),
|
||||||
|
})
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
update(
|
||||||
|
"mm:mm:mm:mm:mm:01".to_string(),
|
||||||
|
"Bob".to_string(),
|
||||||
|
"Laptop".to_string(),
|
||||||
|
"New Vendor".to_string(),
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
let device = read("mm:mm:mm:mm:mm:01".to_string()).unwrap();
|
||||||
|
assert_eq!(device.owner, "Bob".to_string());
|
||||||
|
assert_eq!(device.device_type, "Laptop".to_string());
|
||||||
|
assert_eq!(device.vendor, "New Vendor".to_string());
|
||||||
|
// Sighting and registration fields are untouched
|
||||||
|
assert_eq!(device.ipv4_address, "192.168.250.1".to_string());
|
||||||
|
assert_eq!(device.last_seen, last_seen);
|
||||||
|
assert_eq!(device.name, Some("host.local".to_string()));
|
||||||
|
assert!(device.is_registered);
|
||||||
|
}
|
||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn test_register() {
|
async fn test_register() {
|
||||||
tests_common::setup().await;
|
tests_common::setup().await;
|
||||||
|
|||||||
@@ -25,7 +25,7 @@ fn display_name(device: &Device) -> &str {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Whether a re-sighting represents a real vendor change. A scanner that cannot deduce a vendor
|
// Whether a re-sighting represents a real vendor change. A scanner that cannot deduce a vendor
|
||||||
// reports an empty string; that is not a change (db::devices::update keeps the known vendor), so
|
// reports an empty string; that is not a change (db::devices::seen keeps the known vendor), so
|
||||||
// it must not raise a "vendor changed" notification either. Likewise, first deducing a vendor for a
|
// it must not raise a "vendor changed" notification either. Likewise, first deducing a vendor for a
|
||||||
// device that previously had none is not a change worth notifying about.
|
// device that previously had none is not a change worth notifying about.
|
||||||
fn vendor_changed(existing: &str, new: &str) -> bool {
|
fn vendor_changed(existing: &str, new: &str) -> bool {
|
||||||
|
|||||||
@@ -93,7 +93,13 @@ async fn process_announcement(
|
|||||||
if recorded.name.is_some() {
|
if recorded.name.is_some() {
|
||||||
device.name = recorded.name.clone();
|
device.name = recorded.name.clone();
|
||||||
}
|
}
|
||||||
if let Err(err) = db::devices::update(device.clone()) {
|
if let Err(err) = db::devices::seen(
|
||||||
|
device.mac_address.clone(),
|
||||||
|
device.ipv4_address.clone(),
|
||||||
|
device.vendor.clone(),
|
||||||
|
device.device_type.clone(),
|
||||||
|
device.name.clone(),
|
||||||
|
) {
|
||||||
error!("Failed to update mDNS device {mac}: {err}");
|
error!("Failed to update mDNS device {mac}: {err}");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ use crate::model::devices::{Device, DeviceSummary};
|
|||||||
use crate::model::notifications::{Notification, NotificationType};
|
use crate::model::notifications::{Notification, NotificationType};
|
||||||
use crate::settings::get_settings;
|
use crate::settings::get_settings;
|
||||||
use crate::web_server::arp_scanner::ArpScannerStatusResponse;
|
use crate::web_server::arp_scanner::ArpScannerStatusResponse;
|
||||||
use crate::web_server::devices::RegisterDevicePayload;
|
use crate::web_server::devices::{RegisterDevicePayload, UpdateDevicePayload};
|
||||||
use crate::web_server::mdns_scanner::MdnsScannerStatusResponse;
|
use crate::web_server::mdns_scanner::MdnsScannerStatusResponse;
|
||||||
use axum::Json;
|
use axum::Json;
|
||||||
use axum::extract::Request;
|
use axum::extract::Request;
|
||||||
@@ -43,6 +43,7 @@ pub mod utils;
|
|||||||
devices::summary,
|
devices::summary,
|
||||||
devices::read,
|
devices::read,
|
||||||
devices::register,
|
devices::register,
|
||||||
|
devices::update,
|
||||||
devices::unregister,
|
devices::unregister,
|
||||||
notifications::list,
|
notifications::list,
|
||||||
notifications::read,
|
notifications::read,
|
||||||
@@ -59,6 +60,7 @@ pub mod utils;
|
|||||||
Notification,
|
Notification,
|
||||||
NotificationType,
|
NotificationType,
|
||||||
RegisterDevicePayload,
|
RegisterDevicePayload,
|
||||||
|
UpdateDevicePayload,
|
||||||
DeviceEvent,
|
DeviceEvent,
|
||||||
DeviceEventType,
|
DeviceEventType,
|
||||||
ArpScannerStatusResponse,
|
ArpScannerStatusResponse,
|
||||||
@@ -109,6 +111,7 @@ pub async fn serve() -> Result<(), Box<dyn Error>> {
|
|||||||
.route("/api/devices/summary", get(devices::summary))
|
.route("/api/devices/summary", get(devices::summary))
|
||||||
.route("/api/devices/{mac_address}", delete(devices::unregister))
|
.route("/api/devices/{mac_address}", delete(devices::unregister))
|
||||||
.route("/api/devices/{mac_address}", get(devices::read))
|
.route("/api/devices/{mac_address}", get(devices::read))
|
||||||
|
.route("/api/devices/{mac_address}", put(devices::update))
|
||||||
.route(
|
.route(
|
||||||
"/api/devices/{mac_address}/events",
|
"/api/devices/{mac_address}/events",
|
||||||
get(device_events::list),
|
get(device_events::list),
|
||||||
|
|||||||
@@ -134,6 +134,60 @@ pub async fn register(Json(payload): Json<RegisterDevicePayload>) -> impl IntoRe
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[utoipa::path(
|
||||||
|
put,
|
||||||
|
path = "/api/devices/{mac_address}",
|
||||||
|
tag = "devices",
|
||||||
|
params(
|
||||||
|
("mac_address" = String, Path, description = "MAC address of the device"),
|
||||||
|
),
|
||||||
|
request_body = UpdateDevicePayload,
|
||||||
|
responses(
|
||||||
|
(status = 200, description = "Device updated"),
|
||||||
|
(status = 404, description = "Device not found"),
|
||||||
|
(status = 409, description = "Device is not registered"),
|
||||||
|
(status = 500, description = "Internal server error"),
|
||||||
|
),
|
||||||
|
security(("bearer_auth" = []))
|
||||||
|
)]
|
||||||
|
pub async fn update(
|
||||||
|
Path(mac_address): Path<String>,
|
||||||
|
Json(payload): Json<UpdateDevicePayload>,
|
||||||
|
) -> impl IntoResponse {
|
||||||
|
debug!(
|
||||||
|
"Device update received: mac_address={}, owner={}, device_type={}, vendor={}",
|
||||||
|
mac_address, payload.owner, payload.device_type, payload.vendor
|
||||||
|
);
|
||||||
|
|
||||||
|
let device = match db::devices::read(mac_address.clone()) {
|
||||||
|
Some(value) => value,
|
||||||
|
None => {
|
||||||
|
return (
|
||||||
|
axum::http::StatusCode::NOT_FOUND,
|
||||||
|
"Device not found or could not be read",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
if !device.is_registered {
|
||||||
|
return (
|
||||||
|
axum::http::StatusCode::CONFLICT,
|
||||||
|
"Device is not registered, register it before modifying",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
match db::devices::update(mac_address, payload.owner, payload.device_type, payload.vendor) {
|
||||||
|
Ok(_) => (axum::http::StatusCode::OK, "Device updated"),
|
||||||
|
Err(err) => {
|
||||||
|
error!("Error updating device in the database: {}", err);
|
||||||
|
(
|
||||||
|
axum::http::StatusCode::INTERNAL_SERVER_ERROR,
|
||||||
|
"Error updating device in the server, check your logs",
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[utoipa::path(
|
#[utoipa::path(
|
||||||
delete,
|
delete,
|
||||||
path = "/api/devices/{mac_address}",
|
path = "/api/devices/{mac_address}",
|
||||||
@@ -206,3 +260,10 @@ pub struct RegisterDevicePayload {
|
|||||||
owner: String,
|
owner: String,
|
||||||
device_type: String,
|
device_type: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Deserialize, ToSchema)]
|
||||||
|
pub struct UpdateDevicePayload {
|
||||||
|
owner: String,
|
||||||
|
device_type: String,
|
||||||
|
vendor: String,
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user