diff --git a/src/handlers/http/modal/server.rs b/src/handlers/http/modal/server.rs index d22e5de02..f252531d1 100644 --- a/src/handlers/http/modal/server.rs +++ b/src/handlers/http/modal/server.rs @@ -253,38 +253,47 @@ impl Server { ) } - // get the dashboards web scope pub fn get_dashboards_webscope() -> Scope { web::scope("/dashboards") .service( web::resource("") .route( web::post() - .to(dashboards::post) + .to(dashboards::create_dashboard) .authorize(Action::CreateDashboard), ) .route( web::get() - .to(dashboards::list) + .to(dashboards::list_dashboards) .authorize(Action::ListDashboard), ), ) .service( - web::resource("/{dashboard_id}") - .route( - web::get() - .to(dashboards::get) - .authorize(Action::GetDashboard), - ) - .route( - web::delete() - .to(dashboards::delete) - .authorize(Action::DeleteDashboard), + web::scope("/{dashboard_id}") + .service( + web::resource("") + .route( + web::get() + .to(dashboards::get_dashboard) + .authorize(Action::GetDashboard), + ) + .route( + web::delete() + .to(dashboards::delete_dashboard) + .authorize(Action::DeleteDashboard), + ) + .route( + web::put() + .to(dashboards::update_dashboard) + .authorize(Action::CreateDashboard), + ), ) - .route( - web::put() - .to(dashboards::update) - .authorize(Action::CreateDashboard), + .service( + web::resource("/add_tile").route( + web::put() + .to(dashboards::add_tile) + .authorize(Action::CreateDashboard), + ), ), ) } diff --git a/src/handlers/http/users/dashboards.rs b/src/handlers/http/users/dashboards.rs index 16a885969..93dc4fd7d 100644 --- a/src/handlers/http/users/dashboards.rs +++ b/src/handlers/http/users/dashboards.rs @@ -18,140 +18,131 @@ use crate::{ handlers::http::rbac::RBACError, - parseable::PARSEABLE, - storage::{object_storage::dashboard_path, ObjectStorageError}, - users::dashboards::{Dashboard, CURRENT_DASHBOARD_VERSION, DASHBOARDS}, - utils::{actix::extract_session_key_from_req, get_hash, get_user_from_request}, + storage::ObjectStorageError, + users::dashboards::{validate_dashboard_id, Dashboard, Tile, DASHBOARDS}, + utils::{get_hash, get_user_from_request}, }; use actix_web::{ http::header::ContentType, web::{self, Json, Path}, HttpRequest, HttpResponse, Responder, }; -use bytes::Bytes; -use rand::distributions::DistString; - -use chrono::Utc; use http::StatusCode; use serde_json::Error as SerdeError; -pub async fn list(req: HttpRequest) -> Result { - let key = - extract_session_key_from_req(&req).map_err(|e| DashboardError::Custom(e.to_string()))?; - let dashboards = DASHBOARDS.list_dashboards(&key).await; +pub async fn list_dashboards() -> Result { + let dashboards = DASHBOARDS.list_dashboards().await; + let dashboard_summaries = dashboards + .iter() + .map(|dashboard| dashboard.to_summary()) + .collect::>(); - Ok((web::Json(dashboards), StatusCode::OK)) + Ok((web::Json(dashboard_summaries), StatusCode::OK)) } -pub async fn get( - req: HttpRequest, - dashboard_id: Path, -) -> Result { - let user_id = get_user_from_request(&req)?; - let dashboard_id = dashboard_id.into_inner(); +pub async fn get_dashboard(dashboard_id: Path) -> Result { + let dashboard_id = validate_dashboard_id(dashboard_id.into_inner())?; - if let Some(dashboard) = DASHBOARDS - .get_dashboard(&dashboard_id, &get_hash(&user_id)) + let dashboard = DASHBOARDS + .get_dashboard(dashboard_id) .await - { - return Ok((web::Json(dashboard), StatusCode::OK)); - } + .ok_or_else(|| DashboardError::Metadata("Dashboard does not exist"))?; - Err(DashboardError::Metadata("Dashboard does not exist")) + Ok((web::Json(dashboard), StatusCode::OK)) } -pub async fn post( +pub async fn create_dashboard( req: HttpRequest, Json(mut dashboard): Json, ) -> Result { - let mut user_id = get_user_from_request(&req)?; - user_id = get_hash(&user_id); - let dashboard_id = get_hash(Utc::now().timestamp_micros().to_string().as_str()); - dashboard.dashboard_id = Some(dashboard_id.clone()); - dashboard.version = Some(CURRENT_DASHBOARD_VERSION.to_string()); - - dashboard.user_id = Some(user_id.clone()); - for tile in dashboard.tiles.iter_mut() { - tile.tile_id = Some(get_hash( - format!( - "{}{}", - rand::distributions::Alphanumeric.sample_string(&mut rand::thread_rng(), 8), - Utc::now().timestamp_micros() - ) - .as_str(), - )); + if dashboard.title.is_empty() { + return Err(DashboardError::Metadata("Title must be provided")); } - DASHBOARDS.update(&dashboard).await; - - let path = dashboard_path(&user_id, &format!("{}.json", dashboard_id)); - let store = PARSEABLE.storage.get_object_store(); - let dashboard_bytes = serde_json::to_vec(&dashboard)?; - store - .put_object(&path, Bytes::from(dashboard_bytes)) - .await?; + let user_id = get_hash(&get_user_from_request(&req)?); + DASHBOARDS.create(&user_id, &mut dashboard).await?; Ok((web::Json(dashboard), StatusCode::OK)) } -pub async fn update( +pub async fn update_dashboard( req: HttpRequest, dashboard_id: Path, Json(mut dashboard): Json, ) -> Result { - let mut user_id = get_user_from_request(&req)?; - user_id = get_hash(&user_id); - let dashboard_id = dashboard_id.into_inner(); + let user_id = get_hash(&get_user_from_request(&req)?); + let dashboard_id = validate_dashboard_id(dashboard_id.into_inner())?; - if DASHBOARDS - .get_dashboard(&dashboard_id, &user_id) - .await - .is_none() - { - return Err(DashboardError::Metadata("Dashboard does not exist")); - } - dashboard.dashboard_id = Some(dashboard_id.to_string()); - dashboard.user_id = Some(user_id.clone()); - dashboard.version = Some(CURRENT_DASHBOARD_VERSION.to_string()); - for tile in dashboard.tiles.iter_mut() { - if tile.tile_id.is_none() { - tile.tile_id = Some(get_hash(Utc::now().timestamp_micros().to_string().as_str())); + // Validate all tiles have valid IDs + if let Some(tiles) = &dashboard.tiles { + if tiles.iter().any(|tile| tile.tile_id.is_nil()) { + return Err(DashboardError::Metadata("Tile ID must be provided")); } } - DASHBOARDS.update(&dashboard).await; - let path = dashboard_path(&user_id, &format!("{}.json", dashboard_id)); + // Check if tile_id are unique + if let Some(tiles) = &dashboard.tiles { + let unique_tiles: Vec<_> = tiles + .iter() + .map(|tile| tile.tile_id) + .collect::>() + .into_iter() + .collect(); + + if unique_tiles.len() != tiles.len() { + return Err(DashboardError::Metadata("Tile IDs must be unique")); + } + } - let store = PARSEABLE.storage.get_object_store(); - let dashboard_bytes = serde_json::to_vec(&dashboard)?; - store - .put_object(&path, Bytes::from(dashboard_bytes)) + DASHBOARDS + .update(&user_id, dashboard_id, &mut dashboard) .await?; Ok((web::Json(dashboard), StatusCode::OK)) } -pub async fn delete( +pub async fn delete_dashboard( req: HttpRequest, dashboard_id: Path, ) -> Result { - let mut user_id = get_user_from_request(&req)?; - user_id = get_hash(&user_id); - let dashboard_id = dashboard_id.into_inner(); - if DASHBOARDS - .get_dashboard(&dashboard_id, &user_id) + let user_id = get_hash(&get_user_from_request(&req)?); + let dashboard_id = validate_dashboard_id(dashboard_id.into_inner())?; + + DASHBOARDS.delete_dashboard(&user_id, dashboard_id).await?; + + Ok(HttpResponse::Ok().finish()) +} + +pub async fn add_tile( + req: HttpRequest, + dashboard_id: Path, + Json(tile): Json, +) -> Result { + if tile.tile_id.is_nil() { + return Err(DashboardError::Metadata("Tile ID must be provided")); + } + + let user_id = get_hash(&get_user_from_request(&req)?); + let dashboard_id = validate_dashboard_id(dashboard_id.into_inner())?; + + let mut dashboard = DASHBOARDS + .get_dashboard_by_user(dashboard_id, &user_id) .await - .is_none() - { - return Err(DashboardError::Metadata("Dashboard does not exist")); + .ok_or(DashboardError::Unauthorized)?; + + let tiles = dashboard.tiles.get_or_insert_with(Vec::new); + + // check if the tile already exists + if tiles.iter().any(|t| t.tile_id == tile.tile_id) { + return Err(DashboardError::Metadata("Tile already exists")); } - let path = dashboard_path(&user_id, &format!("{}.json", dashboard_id)); - let store = PARSEABLE.storage.get_object_store(); - store.delete_object(&path).await?; + tiles.push(tile); - DASHBOARDS.delete_dashboard(&dashboard_id).await; + DASHBOARDS + .update(&user_id, dashboard_id, &mut dashboard) + .await?; - Ok(HttpResponse::Ok().finish()) + Ok((web::Json(dashboard), StatusCode::OK)) } #[derive(Debug, thiserror::Error)] @@ -166,6 +157,8 @@ pub enum DashboardError { UserDoesNotExist(#[from] RBACError), #[error("Error: {0}")] Custom(String), + #[error("Dashboard does not exist or is not accessible")] + Unauthorized, } impl actix_web::ResponseError for DashboardError { @@ -176,6 +169,7 @@ impl actix_web::ResponseError for DashboardError { Self::Metadata(_) => StatusCode::BAD_REQUEST, Self::UserDoesNotExist(_) => StatusCode::NOT_FOUND, Self::Custom(_) => StatusCode::INTERNAL_SERVER_ERROR, + Self::Unauthorized => StatusCode::UNAUTHORIZED, } } diff --git a/src/prism/home/mod.rs b/src/prism/home/mod.rs index 656e50cae..9873e1fa9 100644 --- a/src/prism/home/mod.rs +++ b/src/prism/home/mod.rs @@ -410,6 +410,163 @@ async fn get_filter_titles( Ok(filter_titles) } +pub async fn generate_home_search_response( + key: &SessionKey, + query_value: &str, +) -> Result { + let mut resources = Vec::new(); + let (alert_titles, correlation_titles, dashboard_titles, filter_titles, stream_titles) = tokio::join!( + get_alert_titles(key, query_value), + get_correlation_titles(key, query_value), + get_dashboard_titles(query_value), + get_filter_titles(key, query_value), + get_stream_titles(key) + ); + + let alerts = alert_titles?; + resources.extend(alerts); + let correlations = correlation_titles?; + resources.extend(correlations); + let dashboards = dashboard_titles?; + resources.extend(dashboards); + let filters = filter_titles?; + resources.extend(filters); + let stream_titles = stream_titles?; + + for title in stream_titles { + if title.to_lowercase().contains(query_value) { + resources.push(Resource { + id: title.clone(), + name: title, + resource_type: ResourceType::DataSet, + }); + } + } + Ok(HomeSearchResponse { resources }) +} + +// Helper functions to split the work +async fn get_stream_titles(key: &SessionKey) -> Result, PrismHomeError> { + let stream_titles: Vec = PARSEABLE + .storage + .get_object_store() + .list_streams() + .await + .map_err(|e| PrismHomeError::Anyhow(anyhow::Error::new(e)))? + .into_iter() + .filter(|logstream| { + Users.authorize(key.clone(), Action::ListStream, Some(logstream), None) + == crate::rbac::Response::Authorized + }) + .sorted() + .collect_vec(); + + Ok(stream_titles) +} + +async fn get_alert_titles( + key: &SessionKey, + query_value: &str, +) -> Result, PrismHomeError> { + let alerts = ALERTS + .list_alerts_for_user(key.clone()) + .await? + .iter() + .filter_map(|alert| { + if alert.title.to_lowercase().contains(query_value) + || alert.id.to_string().to_lowercase().contains(query_value) + { + Some(Resource { + id: alert.id.to_string(), + name: alert.title.clone(), + resource_type: ResourceType::Alert, + }) + } else { + None + } + }) + .collect_vec(); + + Ok(alerts) +} + +async fn get_correlation_titles( + key: &SessionKey, + query_value: &str, +) -> Result, PrismHomeError> { + let correlations = CORRELATIONS + .list_correlations(key) + .await? + .iter() + .filter_map(|correlation| { + if correlation.title.to_lowercase().contains(query_value) + || correlation.id.to_lowercase().contains(query_value) + { + Some(Resource { + id: correlation.id.to_string(), + name: correlation.title.clone(), + resource_type: ResourceType::Correlation, + }) + } else { + None + } + }) + .collect_vec(); + + Ok(correlations) +} + +async fn get_dashboard_titles(query_value: &str) -> Result, PrismHomeError> { + let dashboard_titles = DASHBOARDS + .list_dashboards() + .await + .iter() + .filter_map(|dashboard| { + let dashboard_id = *dashboard.dashboard_id.as_ref().unwrap(); + let dashboard_id = dashboard_id.to_string(); + if dashboard.title.to_lowercase().contains(query_value) + || dashboard_id.to_lowercase().contains(query_value) + { + Some(Resource { + id: dashboard_id, + name: dashboard.title.clone(), + resource_type: ResourceType::Dashboard, + }) + } else { + None + } + }) + .collect_vec(); + + Ok(dashboard_titles) +} + +async fn get_filter_titles( + key: &SessionKey, + query_value: &str, +) -> Result, PrismHomeError> { + let filter_titles = FILTERS + .list_filters(key) + .await + .iter() + .filter_map(|filter| { + let filter_id = filter.filter_id.as_ref().unwrap().clone(); + if filter.filter_name.to_lowercase().contains(query_value) + || filter_id.to_lowercase().contains(query_value) + { + Some(Resource { + id: filter_id, + name: filter.filter_name.clone(), + resource_type: ResourceType::Filter, + }) + } else { + None + } + }) + .collect_vec(); + Ok(filter_titles) +} + #[derive(Debug, thiserror::Error)] pub enum PrismHomeError { #[error("Error: {0}")] diff --git a/src/users/dashboards.rs b/src/users/dashboards.rs index 8da9f71c0..f02511f3b 100644 --- a/src/users/dashboards.rs +++ b/src/users/dashboards.rs @@ -16,160 +16,143 @@ * */ +use bytes::Bytes; +use chrono::{DateTime, Utc}; use once_cell::sync::Lazy; use serde::{Deserialize, Serialize}; use serde_json::Value; use tokio::sync::RwLock; +use ulid::Ulid; use crate::{ - migration::to_bytes, - parseable::PARSEABLE, - rbac::map::SessionKey, + handlers::http::users::dashboards::DashboardError, parseable::PARSEABLE, storage::object_storage::dashboard_path, - utils::{get_hash, user_auth_for_query}, }; -use super::TimeFilter; - pub static DASHBOARDS: Lazy = Lazy::new(Dashboards::default); -pub const CURRENT_DASHBOARD_VERSION: &str = "v3"; +pub const CURRENT_DASHBOARD_VERSION: &str = "v1"; -#[derive(Debug, Serialize, Deserialize, Default, Clone)] -pub struct Tiles { - name: String, - pub tile_id: Option, - description: String, - query: String, - order: Option, - visualization: Visualization, +#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Eq, Default)] +/// type of dashboard +/// Dashboard is the default type +/// Report is a type of dashboard that is used for reporting +pub enum DashboardType { + /// Dashboard is the default type + #[default] + Dashboard, + /// Report is a type of dashboard that is used for reporting + Report, } #[derive(Debug, Serialize, Deserialize, Default, Clone)] -pub struct Visualization { - visualization_type: String, - circular_chart_config: Option, - graph_config: Option, - size: String, - color_config: Vec, - tick_config: Vec, +pub struct Tile { + pub tile_id: Ulid, + /// all other fields are variable and can be added as needed + #[serde(flatten)] + pub other_fields: Option>, } - #[derive(Debug, Serialize, Deserialize, Default, Clone)] -pub struct CircularChartConfig { - name_key: String, - value_key: String, +pub struct Dashboard { + pub version: Option, + pub title: String, + pub author: Option, + pub dashboard_id: Option, + pub modified: Option>, + dashboard_type: Option, + pub tiles: Option>, } -#[derive(Debug, Serialize, Deserialize, Default, Clone)] -pub struct GraphConfig { - x_key: String, - y_keys: Vec, - graph_type: Option, - orientation: Option, -} +impl Dashboard { + /// set metadata for the dashboard + /// add author, dashboard_id, version, modified, and dashboard_type + /// if dashboard_id is None, generate a new one + pub fn set_metadata(&mut self, user_id: &str, dashboard_id: Option) { + self.author = Some(user_id.to_string()); + self.dashboard_id = dashboard_id.or_else(|| Some(Ulid::new())); + self.version = Some(CURRENT_DASHBOARD_VERSION.to_string()); + self.modified = Some(Utc::now()); + self.dashboard_type = Some(DashboardType::Dashboard); + if self.tiles.is_none() { + self.tiles = Some(Vec::new()); + } + } -#[derive(Debug, Serialize, Deserialize, Default, Clone)] -#[serde(rename_all = "lowercase")] -pub enum GraphType { - #[default] - Default, - Stacked, - Percent, -} + /// create a summary of the dashboard + /// used for listing dashboards + pub fn to_summary(&self) -> serde_json::Map { + let mut map = serde_json::Map::new(); -#[derive(Debug, Serialize, Deserialize, Default, Clone)] -#[serde(rename_all = "lowercase")] -pub enum Orientation { - #[default] - Horizontal, - Vertical, -} + map.insert( + "title".to_string(), + serde_json::Value::String(self.title.clone()), + ); -#[derive(Debug, Serialize, Deserialize, Default, Clone)] -pub struct ColorConfig { - field_name: String, - color_palette: String, -} + if let Some(author) = &self.author { + map.insert( + "author".to_string(), + serde_json::Value::String(author.to_string()), + ); + } -#[derive(Debug, Serialize, Deserialize, Default, Clone)] -pub struct TickConfig { - key: String, - unit: String, + if let Some(modified) = &self.modified { + map.insert( + "modified".to_string(), + serde_json::Value::String(modified.to_string()), + ); + } + + if let Some(dashboard_id) = &self.dashboard_id { + map.insert( + "dashboard_id".to_string(), + serde_json::Value::String(dashboard_id.to_string()), + ); + } + + map + } } -#[derive(Debug, Serialize, Deserialize, Default, Clone)] -pub struct Dashboard { - pub version: Option, - pub name: String, - description: String, - pub dashboard_id: Option, - pub user_id: Option, - pub time_filter: Option, - refresh_interval: u64, - pub tiles: Vec, +/// Validate the dashboard ID +/// Check if the dashboard ID is a valid ULID +/// If the dashboard ID is not valid, return an error +pub fn validate_dashboard_id(dashboard_id: String) -> Result { + Ulid::from_string(&dashboard_id) + .map_err(|_| DashboardError::Metadata("Invalid dashboard ID format - must be a valid ULID")) } #[derive(Default, Debug)] pub struct Dashboards(RwLock>); impl Dashboards { + /// Load all dashboards from the object store + /// and store them in memory + /// This function is called on server start pub async fn load(&self) -> anyhow::Result<()> { let mut this = vec![]; let store = PARSEABLE.storage.get_object_store(); let all_dashboards = store.get_all_dashboards().await.unwrap_or_default(); - for (dashboard_relative_path, dashboards) in all_dashboards { + + for (_, dashboards) in all_dashboards { for dashboard in dashboards { if dashboard.is_empty() { continue; } - let mut dashboard_value = serde_json::from_slice::(&dashboard)?; - if let Some(meta) = dashboard_value.clone().as_object() { - let version = meta.get("version").and_then(|version| version.as_str()); - let dashboard_id = meta - .get("dashboard_id") - .and_then(|dashboard_id| dashboard_id.as_str()); - match version { - Some("v1") => { - //delete older version of the dashboard - store.delete_object(&dashboard_relative_path).await?; - - dashboard_value = migrate_v1_v2(dashboard_value); - dashboard_value = migrate_v2_v3(dashboard_value); - let user_id = dashboard_value - .as_object() - .unwrap() - .get("user_id") - .and_then(|user_id| user_id.as_str()); - let path = dashboard_path( - user_id.unwrap(), - &format!("{}.json", dashboard_id.unwrap()), - ); - let dashboard_bytes = to_bytes(&dashboard_value); - store.put_object(&path, dashboard_bytes.clone()).await?; - } - Some("v2") => { - //delete older version of the dashboard - store.delete_object(&dashboard_relative_path).await?; - - dashboard_value = migrate_v2_v3(dashboard_value); - let user_id = dashboard_value - .as_object() - .unwrap() - .get("user_id") - .and_then(|user_id| user_id.as_str()); - let path = dashboard_path( - user_id.unwrap(), - &format!("{}.json", dashboard_id.unwrap()), - ); - let dashboard_bytes = to_bytes(&dashboard_value); - store.put_object(&path, dashboard_bytes.clone()).await?; - } - _ => {} + + let dashboard_value = match serde_json::from_slice::(&dashboard) + { + Ok(value) => value, + Err(err) => { + tracing::warn!("Failed to parse dashboard JSON: {}", err); + continue; } - } - if let Ok(dashboard) = serde_json::from_value::(dashboard_value) { + }; + + if let Ok(dashboard) = serde_json::from_value::(dashboard_value.clone()) + { this.retain(|d: &Dashboard| d.dashboard_id != dashboard.dashboard_id); this.push(dashboard); + } else { + tracing::warn!("Failed to deserialize dashboard: {:?}", dashboard_value); } } } @@ -180,113 +163,140 @@ impl Dashboards { Ok(()) } - pub async fn update(&self, dashboard: &Dashboard) { - let mut s = self.0.write().await; - s.retain(|d| d.dashboard_id != dashboard.dashboard_id); - s.push(dashboard.clone()); + /// Save the dashboard to the object store + /// This function is called when creating or updating a dashboard + async fn save_dashboard( + &self, + user_id: &str, + dashboard: &Dashboard, + ) -> Result<(), DashboardError> { + let dashboard_id = dashboard + .dashboard_id + .ok_or(DashboardError::Metadata("Dashboard ID must be provided"))?; + let path = dashboard_path(user_id, &format!("{}.json", dashboard_id)); + + let store = PARSEABLE.storage.get_object_store(); + let dashboard_bytes = serde_json::to_vec(&dashboard)?; + store + .put_object(&path, Bytes::from(dashboard_bytes)) + .await?; + + Ok(()) } - pub async fn delete_dashboard(&self, dashboard_id: &str) { - let mut s = self.0.write().await; - s.retain(|d| d.dashboard_id != Some(dashboard_id.to_string())); + /// Create a new dashboard + /// This function is called when creating a new dashboard + /// add dashboard in memory and save it to the object store + pub async fn create( + &self, + user_id: &str, + dashboard: &mut Dashboard, + ) -> Result<(), DashboardError> { + dashboard.set_metadata(user_id, None); + + self.save_dashboard(user_id, dashboard).await?; + self.0.write().await.push(dashboard.clone()); + + Ok(()) + } + + /// Update an existing dashboard + /// This function is called when updating a dashboard + /// update dashboard in memory and save it to the object store + pub async fn update( + &self, + user_id: &str, + dashboard_id: Ulid, + dashboard: &mut Dashboard, + ) -> Result<(), DashboardError> { + self.ensure_dashboard_ownership(dashboard_id, user_id) + .await?; + + dashboard.set_metadata(user_id, Some(dashboard_id)); + self.save_dashboard(user_id, dashboard).await?; + + let mut dashboards = self.0.write().await; + dashboards.retain(|d| d.dashboard_id != dashboard.dashboard_id); + dashboards.push(dashboard.clone()); + + Ok(()) } - pub async fn get_dashboard(&self, dashboard_id: &str, user_id: &str) -> Option { + /// Delete a dashboard + /// This function is called when deleting a dashboard + /// delete dashboard in memory and from the object store + pub async fn delete_dashboard( + &self, + user_id: &str, + dashboard_id: Ulid, + ) -> Result<(), DashboardError> { + self.ensure_dashboard_ownership(dashboard_id, user_id) + .await?; + + let path = dashboard_path(user_id, &format!("{}.json", dashboard_id)); + let store = PARSEABLE.storage.get_object_store(); + store.delete_object(&path).await?; + + self.0 + .write() + .await + .retain(|d| d.dashboard_id.as_ref().is_none_or(|id| *id != dashboard_id)); + + Ok(()) + } + + /// Get a dashboard by ID + /// fetch dashboard from memory + pub async fn get_dashboard(&self, dashboard_id: Ulid) -> Option { self.0 .read() .await .iter() .find(|d| { - d.dashboard_id == Some(dashboard_id.to_string()) - && d.user_id == Some(user_id.to_string()) + d.dashboard_id + .as_ref() + .is_some_and(|id| *id == dashboard_id) }) .cloned() } - pub async fn list_dashboards(&self, key: &SessionKey) -> Vec { - let read = self.0.read().await; - - let mut dashboards = Vec::new(); - - for d in read.iter() { - let mut skip_dashboard = false; - for tile in d.tiles.iter() { - let query = &tile.query; - match user_auth_for_query(key, query).await { - Ok(_) => {} - Err(_) => { - skip_dashboard = true; - break; - } - } - } - if !skip_dashboard { - dashboards.push(d.clone()); - } - } - dashboards + /// Get a dashboard by ID and user ID + /// fetch dashboard from memory + pub async fn get_dashboard_by_user( + &self, + dashboard_id: Ulid, + user_id: &str, + ) -> Option { + self.0 + .read() + .await + .iter() + .find(|d| { + d.dashboard_id + .as_ref() + .is_some_and(|id| *id == dashboard_id) + && d.author == Some(user_id.to_string()) + }) + .cloned() } -} -fn migrate_v1_v2(mut dashboard_meta: Value) -> Value { - let dashboard_meta_map = dashboard_meta.as_object_mut().unwrap(); - let user_id = dashboard_meta_map.get("user_id").unwrap().clone(); - let str_user_id = user_id.as_str().unwrap(); - let user_id_hash = get_hash(str_user_id); - dashboard_meta_map.insert("user_id".to_owned(), Value::String(user_id_hash)); - dashboard_meta_map.insert( - "version".to_owned(), - Value::String(CURRENT_DASHBOARD_VERSION.into()), - ); - let tiles = dashboard_meta_map - .get_mut("tiles") - .unwrap() - .as_array_mut() - .unwrap(); - for tile in tiles.iter_mut() { - let tile_map = tile.as_object_mut().unwrap(); - let visualization = tile_map - .get_mut("visualization") - .unwrap() - .as_object_mut() - .unwrap(); - visualization.insert("tick_config".to_owned(), Value::Array(vec![])); + /// List all dashboards + /// fetch all dashboards from memory + pub async fn list_dashboards(&self) -> Vec { + self.0.read().await.clone() } - dashboard_meta -} - -fn migrate_v2_v3(mut dashboard_meta: Value) -> Value { - let dashboard_meta_map = dashboard_meta.as_object_mut().unwrap(); - - dashboard_meta_map.insert( - "version".to_owned(), - Value::String(CURRENT_DASHBOARD_VERSION.into()), - ); - let tiles = dashboard_meta_map - .get_mut("tiles") - .unwrap() - .as_array_mut() - .unwrap(); - for tile in tiles { - let tile_map = tile.as_object_mut().unwrap(); - let visualization = tile_map - .get_mut("visualization") - .unwrap() - .as_object_mut() - .unwrap(); - if visualization.get("graph_config").is_some() - && !visualization.get("graph_config").unwrap().is_null() - { - let graph_config = visualization - .get_mut("graph_config") - .unwrap() - .as_object_mut() - .unwrap(); - graph_config.insert("orientation".to_owned(), Value::String("horizontal".into())); - graph_config.insert("graph_type".to_owned(), Value::String("default".into())); - } + /// Ensure the user is the owner of the dashboard + /// This function is called when updating or deleting a dashboard + /// check if the user is the owner of the dashboard + /// if the user is not the owner, return an error + async fn ensure_dashboard_ownership( + &self, + dashboard_id: Ulid, + user_id: &str, + ) -> Result { + self.get_dashboard_by_user(dashboard_id, user_id) + .await + .ok_or(DashboardError::Unauthorized) } - - dashboard_meta }