mirror of
https://github.com/rzuasti/oott.git
synced 2026-07-08 19:21:54 +02:00
Add device_events table and scan history API endpoint
Records a DeviceEvent row on every scan: NewDevice when a device is first
seen, DeviceSeen on subsequent scans. Exposes GET /api/devices/{mac}/events
with pagination, ordered by date descending.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Sonnet 4.6
parent
47a76c4bf8
commit
888f18603f
@@ -0,0 +1,8 @@
|
|||||||
|
CREATE TABLE device_events(
|
||||||
|
id INTEGER PRIMARY KEY,
|
||||||
|
mac_address TEXT NOT NULL,
|
||||||
|
created_on TEXT NOT NULL,
|
||||||
|
event_type TEXT NOT NULL,
|
||||||
|
ipv4_address TEXT NOT NULL,
|
||||||
|
vendor TEXT NOT NULL
|
||||||
|
);
|
||||||
@@ -1,3 +1,4 @@
|
|||||||
|
pub mod device_events;
|
||||||
pub mod devices;
|
pub mod devices;
|
||||||
pub mod error;
|
pub mod error;
|
||||||
pub mod notifications;
|
pub mod notifications;
|
||||||
|
|||||||
@@ -0,0 +1,223 @@
|
|||||||
|
use crate::db;
|
||||||
|
use crate::db::error::DbError;
|
||||||
|
use log::{debug, error};
|
||||||
|
use rusqlite::{params, params_from_iter};
|
||||||
|
|
||||||
|
use crate::model::device_events::DeviceEvent;
|
||||||
|
|
||||||
|
pub fn insert(event: DeviceEvent) -> Result<i64, DbError> {
|
||||||
|
let conn = db::get_db_connection();
|
||||||
|
|
||||||
|
match conn.execute(
|
||||||
|
"INSERT INTO device_events (mac_address, created_on, event_type, ipv4_address, vendor) VALUES (?1, ?2, ?3, ?4, ?5)",
|
||||||
|
params![
|
||||||
|
event.mac_address,
|
||||||
|
event.created_on,
|
||||||
|
event.event_type,
|
||||||
|
event.ipv4_address,
|
||||||
|
event.vendor
|
||||||
|
],
|
||||||
|
) {
|
||||||
|
Ok(_) => {
|
||||||
|
debug!("Device event inserted into database: {}", event);
|
||||||
|
Ok(conn.last_insert_rowid())
|
||||||
|
}
|
||||||
|
Err(error) => {
|
||||||
|
error!("Error inserting device event ({event}) into database: {error}");
|
||||||
|
Err(DbError::from(error))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn list(
|
||||||
|
mac_address: Option<String>,
|
||||||
|
page_offset: Option<i64>,
|
||||||
|
page_limit: Option<i64>,
|
||||||
|
) -> Result<Vec<DeviceEvent>, DbError> {
|
||||||
|
debug!("Listing device events");
|
||||||
|
let conn = db::get_db_connection();
|
||||||
|
|
||||||
|
let mut sql_statement =
|
||||||
|
"SELECT id, mac_address, created_on, event_type, ipv4_address, vendor FROM device_events WHERE 1=1"
|
||||||
|
.to_string();
|
||||||
|
|
||||||
|
let mut params: Vec<rusqlite::types::Value> = Vec::new();
|
||||||
|
|
||||||
|
if let Some(mac) = mac_address {
|
||||||
|
debug!("Adding filter mac_address={}", mac);
|
||||||
|
sql_statement.push_str(" AND mac_address=?");
|
||||||
|
params.push(mac.into());
|
||||||
|
}
|
||||||
|
|
||||||
|
sql_statement.push_str(" ORDER BY created_on DESC");
|
||||||
|
|
||||||
|
if let (Some(page_offset), Some(page_limit)) = (page_offset, page_limit) {
|
||||||
|
debug!(
|
||||||
|
"Adding paging with offset={} and limit={}",
|
||||||
|
page_offset, page_limit
|
||||||
|
);
|
||||||
|
sql_statement.push_str(" LIMIT ? OFFSET ?");
|
||||||
|
params.push(page_limit.into());
|
||||||
|
params.push(page_offset.into());
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut stmt = conn.prepare(sql_statement.as_str())?;
|
||||||
|
|
||||||
|
let events: Vec<DeviceEvent> = stmt
|
||||||
|
.query_map(params_from_iter(params.iter()), |row| {
|
||||||
|
Ok(DeviceEvent {
|
||||||
|
id: row.get(0)?,
|
||||||
|
mac_address: row.get(1)?,
|
||||||
|
created_on: row.get(2)?,
|
||||||
|
event_type: row.get(3)?,
|
||||||
|
ipv4_address: row.get(4)?,
|
||||||
|
vendor: row.get(5)?,
|
||||||
|
})
|
||||||
|
})?
|
||||||
|
.collect::<Result<_, _>>()?;
|
||||||
|
|
||||||
|
Ok(events)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
fn read(id: i64) -> Option<DeviceEvent> {
|
||||||
|
let conn = db::get_db_connection();
|
||||||
|
|
||||||
|
let result: Result<DeviceEvent, rusqlite::Error> = conn.query_one(
|
||||||
|
"SELECT id, mac_address, created_on, event_type, ipv4_address, vendor FROM device_events WHERE id=?1",
|
||||||
|
params![id],
|
||||||
|
|row| {
|
||||||
|
Ok(DeviceEvent {
|
||||||
|
id: row.get(0)?,
|
||||||
|
mac_address: row.get(1)?,
|
||||||
|
created_on: row.get(2)?,
|
||||||
|
event_type: row.get(3)?,
|
||||||
|
ipv4_address: row.get(4)?,
|
||||||
|
vendor: row.get(5)?,
|
||||||
|
})
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
match result {
|
||||||
|
Ok(value) => Some(value),
|
||||||
|
Err(error) => {
|
||||||
|
match error {
|
||||||
|
rusqlite::Error::QueryReturnedNoRows => {
|
||||||
|
debug!("No device event found for id={id}.")
|
||||||
|
}
|
||||||
|
_ => error!("Error reading device event from database: {error}"),
|
||||||
|
};
|
||||||
|
None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use chrono::Utc;
|
||||||
|
|
||||||
|
use super::*;
|
||||||
|
use crate::{model::device_events::DeviceEventType, tests_common};
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn test_insert() {
|
||||||
|
tests_common::setup().await;
|
||||||
|
|
||||||
|
let created_on = Utc::now();
|
||||||
|
let inserted_id = insert(DeviceEvent::new(
|
||||||
|
"aa:aa:aa:aa:aa:aa".to_string(),
|
||||||
|
created_on,
|
||||||
|
DeviceEventType::NewDevice,
|
||||||
|
"192.168.0.1".to_string(),
|
||||||
|
"Vendor 1".to_string(),
|
||||||
|
))
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
assert!(inserted_id >= 0, "Inserted device event id should be positive");
|
||||||
|
|
||||||
|
let event = read(inserted_id).unwrap();
|
||||||
|
assert_eq!(event.mac_address, "aa:aa:aa:aa:aa:aa");
|
||||||
|
assert_eq!(event.created_on, created_on);
|
||||||
|
assert_eq!(event.event_type, DeviceEventType::NewDevice);
|
||||||
|
assert_eq!(event.ipv4_address, "192.168.0.1");
|
||||||
|
assert_eq!(event.vendor, "Vendor 1");
|
||||||
|
|
||||||
|
let inserted_id = insert(DeviceEvent::new(
|
||||||
|
"bb:bb:bb:bb:bb:bb".to_string(),
|
||||||
|
Utc::now(),
|
||||||
|
DeviceEventType::DeviceSeen,
|
||||||
|
"192.168.0.2".to_string(),
|
||||||
|
"Vendor 2".to_string(),
|
||||||
|
))
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
let event = read(inserted_id).unwrap();
|
||||||
|
assert_eq!(event.event_type, DeviceEventType::DeviceSeen);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn test_list() {
|
||||||
|
tests_common::setup().await;
|
||||||
|
|
||||||
|
let events = list(None, None, None).unwrap();
|
||||||
|
assert!(
|
||||||
|
events.len() >= 3,
|
||||||
|
"There should be at least 3 device events from seed data"
|
||||||
|
);
|
||||||
|
|
||||||
|
// Verify ordering: created_on DESC — first element should be newest
|
||||||
|
if events.len() >= 2 {
|
||||||
|
assert!(
|
||||||
|
events[0].created_on >= events[1].created_on,
|
||||||
|
"Events should be ordered by created_on DESC"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
let event3 = events.iter().find(|e| e.id == 3).unwrap();
|
||||||
|
assert_eq!(event3.mac_address, "aa:aa:aa:aa:aa:aa");
|
||||||
|
assert_eq!(event3.event_type, DeviceEventType::DeviceSeen);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn test_list_filter_by_mac_address() {
|
||||||
|
tests_common::setup().await;
|
||||||
|
|
||||||
|
let events = list(Some("aa:aa:aa:aa:aa:aa".to_string()), None, None).unwrap();
|
||||||
|
assert!(
|
||||||
|
events.len() >= 2,
|
||||||
|
"There should be at least 2 events for aa:aa:aa:aa:aa:aa"
|
||||||
|
);
|
||||||
|
for event in &events {
|
||||||
|
assert_eq!(
|
||||||
|
event.mac_address, "aa:aa:aa:aa:aa:aa",
|
||||||
|
"All returned events should belong to mac_address aa:aa:aa:aa:aa:aa"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
let events = list(Some("zz:zz:zz:zz:zz:zz".to_string()), None, None).unwrap();
|
||||||
|
assert!(events.is_empty(), "Unknown MAC should return empty list");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn test_list_pagination() {
|
||||||
|
tests_common::setup().await;
|
||||||
|
|
||||||
|
let first_page = list(None, Some(0), Some(2)).unwrap();
|
||||||
|
assert_eq!(first_page.len(), 2, "First page should have 2 events");
|
||||||
|
|
||||||
|
let second_page = list(None, Some(2), Some(2)).unwrap();
|
||||||
|
assert!(
|
||||||
|
second_page.len() >= 1,
|
||||||
|
"Second page should have at least 1 event"
|
||||||
|
);
|
||||||
|
|
||||||
|
let first_ids: Vec<i64> = first_page.iter().map(|e| e.id).collect();
|
||||||
|
for event in &second_page {
|
||||||
|
assert!(
|
||||||
|
!first_ids.contains(&event.id),
|
||||||
|
"Event id={} should not appear in both pages",
|
||||||
|
event.id
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+24
-1
@@ -5,13 +5,14 @@ use std::error::Error;
|
|||||||
use std::time::Duration;
|
use std::time::Duration;
|
||||||
|
|
||||||
use crate::db;
|
use crate::db;
|
||||||
|
use crate::model::device_events::{DeviceEvent, DeviceEventType};
|
||||||
use crate::model::devices::Device;
|
use crate::model::devices::Device;
|
||||||
use crate::model::notifications::Notification;
|
use crate::model::notifications::Notification;
|
||||||
use crate::model::notifications::NotificationType;
|
use crate::model::notifications::NotificationType;
|
||||||
use crate::settings::get_settings;
|
use crate::settings::get_settings;
|
||||||
use chrono::{Local, Utc};
|
use chrono::{Local, Utc};
|
||||||
use duration_string::DurationString;
|
use duration_string::DurationString;
|
||||||
use log::{debug, info, warn};
|
use log::{debug, error, info, warn};
|
||||||
|
|
||||||
// Private helper function to deliver messages
|
// Private helper function to deliver messages
|
||||||
fn send_notification(notification: Notification) -> Result<(), Box<dyn Error>> {
|
fn send_notification(notification: Notification) -> Result<(), Box<dyn Error>> {
|
||||||
@@ -34,6 +35,17 @@ fn send_notification(notification: Notification) -> Result<(), Box<dyn Error>> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub fn trigger_new_device(device: Device) -> Result<(), Box<dyn Error>> {
|
pub fn trigger_new_device(device: Device) -> Result<(), Box<dyn Error>> {
|
||||||
|
let event = DeviceEvent::new(
|
||||||
|
device.mac_address.clone(),
|
||||||
|
Utc::now(),
|
||||||
|
DeviceEventType::NewDevice,
|
||||||
|
device.ipv4_address.clone(),
|
||||||
|
device.vendor.clone(),
|
||||||
|
);
|
||||||
|
if let Err(err) = db::device_events::insert(event) {
|
||||||
|
error!("Failed to record device event for {}: {}", device.mac_address, err);
|
||||||
|
}
|
||||||
|
|
||||||
let notification = Notification::new(
|
let notification = Notification::new(
|
||||||
Utc::now(),
|
Utc::now(),
|
||||||
NotificationType::NewDeviceFound,
|
NotificationType::NewDeviceFound,
|
||||||
@@ -54,6 +66,17 @@ pub fn trigger_existing_device(
|
|||||||
existing_device: Device,
|
existing_device: Device,
|
||||||
new_device: Device,
|
new_device: Device,
|
||||||
) -> Result<(), Box<dyn Error>> {
|
) -> Result<(), Box<dyn Error>> {
|
||||||
|
let event = DeviceEvent::new(
|
||||||
|
new_device.mac_address.clone(),
|
||||||
|
Utc::now(),
|
||||||
|
DeviceEventType::DeviceSeen,
|
||||||
|
new_device.ipv4_address.clone(),
|
||||||
|
new_device.vendor.clone(),
|
||||||
|
);
|
||||||
|
if let Err(err) = db::device_events::insert(event) {
|
||||||
|
error!("Failed to record device event for {}: {}", new_device.mac_address, err);
|
||||||
|
}
|
||||||
|
|
||||||
// Notify if the device comes back online after not being seen for the configured period
|
// Notify if the device comes back online after not being seen for the configured period
|
||||||
let elapsed_since_last_seen: Duration = (Local::now().to_utc() - existing_device.last_seen)
|
let elapsed_since_last_seen: Duration = (Local::now().to_utc() - existing_device.last_seen)
|
||||||
.to_std()
|
.to_std()
|
||||||
|
|||||||
@@ -1,2 +1,3 @@
|
|||||||
|
pub mod device_events;
|
||||||
pub mod devices;
|
pub mod devices;
|
||||||
pub mod notifications;
|
pub mod notifications;
|
||||||
|
|||||||
@@ -0,0 +1,111 @@
|
|||||||
|
use crate::utils::date_serializer;
|
||||||
|
use chrono::{DateTime, Utc};
|
||||||
|
use rusqlite::types::{FromSql, FromSqlError, FromSqlResult, ToSql, ToSqlOutput, ValueRef};
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
use std::{error::Error, fmt, str::FromStr};
|
||||||
|
use utoipa::ToSchema;
|
||||||
|
|
||||||
|
#[derive(Clone, Serialize, Deserialize, ToSchema)]
|
||||||
|
pub struct DeviceEvent {
|
||||||
|
pub id: i64,
|
||||||
|
pub mac_address: String,
|
||||||
|
#[serde(with = "date_serializer")]
|
||||||
|
#[schema(value_type = String, format = DateTime)]
|
||||||
|
pub created_on: DateTime<Utc>,
|
||||||
|
pub event_type: DeviceEventType,
|
||||||
|
pub ipv4_address: String,
|
||||||
|
pub vendor: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl DeviceEvent {
|
||||||
|
pub fn new(
|
||||||
|
mac_address: String,
|
||||||
|
created_on: DateTime<Utc>,
|
||||||
|
event_type: DeviceEventType,
|
||||||
|
ipv4_address: String,
|
||||||
|
vendor: String,
|
||||||
|
) -> Self {
|
||||||
|
Self {
|
||||||
|
id: -1,
|
||||||
|
mac_address,
|
||||||
|
created_on,
|
||||||
|
event_type,
|
||||||
|
ipv4_address,
|
||||||
|
vendor,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl fmt::Display for DeviceEvent {
|
||||||
|
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||||
|
write!(
|
||||||
|
f,
|
||||||
|
"id={}, mac_address={}, created_on={}, event_type={}, ipv4_address={}, vendor={}",
|
||||||
|
self.id,
|
||||||
|
self.mac_address,
|
||||||
|
self.created_on,
|
||||||
|
self.event_type,
|
||||||
|
self.ipv4_address,
|
||||||
|
self.vendor,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl PartialEq for DeviceEvent {
|
||||||
|
fn eq(&self, other: &Self) -> bool {
|
||||||
|
self.id == other.id
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, ToSchema)]
|
||||||
|
pub enum DeviceEventType {
|
||||||
|
NewDevice,
|
||||||
|
DeviceSeen,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl fmt::Display for DeviceEventType {
|
||||||
|
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||||
|
match self {
|
||||||
|
Self::NewDevice => write!(f, "NewDevice"),
|
||||||
|
Self::DeviceSeen => write!(f, "DeviceSeen"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub struct DeviceEventTypeParseError;
|
||||||
|
|
||||||
|
impl fmt::Display for DeviceEventTypeParseError {
|
||||||
|
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||||
|
write!(f, "Error parsing device event type")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Error for DeviceEventTypeParseError {}
|
||||||
|
|
||||||
|
impl FromStr for DeviceEventType {
|
||||||
|
type Err = DeviceEventTypeParseError;
|
||||||
|
|
||||||
|
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||||||
|
match s {
|
||||||
|
"NewDevice" => Ok(DeviceEventType::NewDevice),
|
||||||
|
"DeviceSeen" => Ok(DeviceEventType::DeviceSeen),
|
||||||
|
_ => Err(DeviceEventTypeParseError),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ToSql for DeviceEventType {
|
||||||
|
fn to_sql(&self) -> rusqlite::Result<ToSqlOutput<'_>> {
|
||||||
|
Ok(self.to_string().into())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl FromSql for DeviceEventType {
|
||||||
|
fn column_result(value: ValueRef<'_>) -> FromSqlResult<Self> {
|
||||||
|
value
|
||||||
|
.as_str()?
|
||||||
|
.parse()
|
||||||
|
.map_err(|e| FromSqlError::Other(Box::new(e)))
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,5 +1,6 @@
|
|||||||
use std::error::Error;
|
use std::error::Error;
|
||||||
|
|
||||||
|
use crate::model::device_events::{DeviceEvent, DeviceEventType};
|
||||||
use crate::model::devices::Device;
|
use crate::model::devices::Device;
|
||||||
use crate::model::notifications::{Notification, NotificationType};
|
use crate::model::notifications::{Notification, NotificationType};
|
||||||
use crate::settings::get_settings;
|
use crate::settings::get_settings;
|
||||||
@@ -20,6 +21,7 @@ use utoipa::openapi::security::{HttpAuthScheme, HttpBuilder, SecurityScheme};
|
|||||||
use utoipa::Modify;
|
use utoipa::Modify;
|
||||||
use utoipa_swagger_ui::SwaggerUi;
|
use utoipa_swagger_ui::SwaggerUi;
|
||||||
|
|
||||||
|
pub mod device_events;
|
||||||
pub mod devices;
|
pub mod devices;
|
||||||
pub mod notifications;
|
pub mod notifications;
|
||||||
pub mod utils;
|
pub mod utils;
|
||||||
@@ -42,17 +44,21 @@ pub mod utils;
|
|||||||
notifications::read_without_flagging,
|
notifications::read_without_flagging,
|
||||||
notifications::mark_as_new,
|
notifications::mark_as_new,
|
||||||
notifications::mark_all_as_old,
|
notifications::mark_all_as_old,
|
||||||
|
device_events::list,
|
||||||
),
|
),
|
||||||
components(schemas(
|
components(schemas(
|
||||||
Device,
|
Device,
|
||||||
Notification,
|
Notification,
|
||||||
NotificationType,
|
NotificationType,
|
||||||
RegisterDevicePayload,
|
RegisterDevicePayload,
|
||||||
|
DeviceEvent,
|
||||||
|
DeviceEventType,
|
||||||
)),
|
)),
|
||||||
modifiers(&SecurityAddon),
|
modifiers(&SecurityAddon),
|
||||||
tags(
|
tags(
|
||||||
(name = "devices", description = "Device management"),
|
(name = "devices", description = "Device management"),
|
||||||
(name = "notifications", description = "Notification management"),
|
(name = "notifications", description = "Notification management"),
|
||||||
|
(name = "device_events", description = "Device event history"),
|
||||||
)
|
)
|
||||||
)]
|
)]
|
||||||
struct ApiDoc;
|
struct ApiDoc;
|
||||||
@@ -94,6 +100,7 @@ pub async fn serve() -> Result<(), Box<dyn Error>> {
|
|||||||
.route("/api/devices", put(devices::register))
|
.route("/api/devices", put(devices::register))
|
||||||
.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}/events", get(device_events::list))
|
||||||
.route("/api/notifications", get(notifications::list))
|
.route("/api/notifications", get(notifications::list))
|
||||||
.route(
|
.route(
|
||||||
"/api/notifications/mark_all_as_old",
|
"/api/notifications/mark_all_as_old",
|
||||||
|
|||||||
@@ -0,0 +1,41 @@
|
|||||||
|
use std::collections::HashMap;
|
||||||
|
|
||||||
|
use axum::{
|
||||||
|
Json,
|
||||||
|
extract::{Path, Query},
|
||||||
|
http::StatusCode,
|
||||||
|
};
|
||||||
|
use log::error;
|
||||||
|
|
||||||
|
use crate::{db, model::device_events::DeviceEvent, web_server::utils};
|
||||||
|
|
||||||
|
#[utoipa::path(
|
||||||
|
get,
|
||||||
|
path = "/api/devices/{mac_address}/events",
|
||||||
|
tag = "device_events",
|
||||||
|
params(
|
||||||
|
("mac_address" = String, Path, description = "MAC address of the device"),
|
||||||
|
("page_offset" = Option<i64>, Query, description = "Pagination offset"),
|
||||||
|
("page_limit" = Option<i64>, Query, description = "Maximum number of results to return"),
|
||||||
|
),
|
||||||
|
responses(
|
||||||
|
(status = 200, description = "List of device events", body = Vec<DeviceEvent>),
|
||||||
|
(status = 500, description = "Internal server error"),
|
||||||
|
),
|
||||||
|
security(("bearer_auth" = []))
|
||||||
|
)]
|
||||||
|
pub async fn list(
|
||||||
|
Path(mac_address): Path<String>,
|
||||||
|
Query(params): Query<HashMap<String, String>>,
|
||||||
|
) -> Result<Json<Vec<DeviceEvent>>, StatusCode> {
|
||||||
|
let page_offset: Option<i64> = utils::parse_parameter_int(¶ms, "page_offset");
|
||||||
|
let page_limit: Option<i64> = utils::parse_parameter_int(¶ms, "page_limit");
|
||||||
|
|
||||||
|
match db::device_events::list(Some(mac_address), page_offset, page_limit) {
|
||||||
|
Ok(value) => Ok(Json(value)),
|
||||||
|
Err(err) => {
|
||||||
|
error!("Error listing device events: {}", err);
|
||||||
|
Err(StatusCode::INTERNAL_SERVER_ERROR)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
INSERT INTO device_events (id, mac_address, created_on, event_type, ipv4_address, vendor)
|
||||||
|
VALUES (1, 'aa:aa:aa:aa:aa:aa', '2026-01-01 11:11:11', 'NewDevice', '192.168.0.1', 'Vendor 1');
|
||||||
|
INSERT INTO device_events (id, mac_address, created_on, event_type, ipv4_address, vendor)
|
||||||
|
VALUES (2, 'bb:bb:bb:bb:bb:bb', '2026-02-03 13:14:15', 'DeviceSeen', '192.168.0.2', 'Vendor 2');
|
||||||
|
INSERT INTO device_events (id, mac_address, created_on, event_type, ipv4_address, vendor)
|
||||||
|
VALUES (3, 'aa:aa:aa:aa:aa:aa', '2026-03-10 09:00:00', 'DeviceSeen', '192.168.0.1', 'Vendor 1');
|
||||||
Reference in New Issue
Block a user