diff --git a/.env b/.env index 8e71f33d..ca5d5019 100644 --- a/.env +++ b/.env @@ -2,6 +2,11 @@ ENV=DEV # PROD or DEV ONLY_RELAY_SERVICE=FALSE # TRUE - This will start only nightly relay service without cloud service NONCE=VERY_SECRET_NONCE MAILER_ADDRESS = do_not_reply@nightly.app +GRAFANA_BASE_PATH = http://localhost:3000/api +# TEST LOGIN DO NO USE IN PRODUCTION +GRAFANA_CLIENT_LOGIN = admin +# TEST PASSWORD DO NO USE IN PRODUCTION +GRAFANA_CLIENT_PASSWORD = admin # TEST PASSWORD DO NO USE IN PRODUCTION MAILER_PASSWORD = A12indqww021dD # Generated so it can work with grafana diff --git a/grafana/grafana.ini b/grafana/grafana.ini index 462b36b4..da7a7388 100644 --- a/grafana/grafana.ini +++ b/grafana/grafana.ini @@ -5,7 +5,4 @@ email_claim = sub username_claim = sub auto_sign_up = true key_file = /etc/grafana/public-key.pem -url_login = true - -[auth.basic] -enabled = false \ No newline at end of file +url_login = true \ No newline at end of file diff --git a/server/src/cloud_state.rs b/server/src/cloud_state.rs index 3d1c4bc5..3bdf71a8 100644 --- a/server/src/cloud_state.rs +++ b/server/src/cloud_state.rs @@ -1,5 +1,5 @@ use crate::{ - env::ENVIRONMENT, + env::{ENVIRONMENT, GRAFANA_BASE_PATH, GRAFANA_CLIENT_LOGIN, GRAFANA_CLIENT_PASSWORD}, ip_geolocation::GeolocationRequester, mailer::{entry::run_mailer, mailer::Mailer}, structs::session_cache::ApiSessionsCache, @@ -10,6 +10,7 @@ use hickory_resolver::{ name_server::{GenericConnector, TokioRuntimeProvider}, AsyncResolver, TokioAsyncResolver, }; +use openapi::apis::configuration::Configuration; use r_cache::cache::Cache; use reqwest::Url; use std::{sync::Arc, time::Duration}; @@ -26,6 +27,7 @@ pub struct CloudState { pub mailer: Arc, pub dns_resolver: Arc, pub webauthn: Arc, + pub grafana_client_conf: Arc, } impl CloudState { @@ -36,6 +38,15 @@ impl CloudState { let mailer = Arc::new(run_mailer().await.unwrap()); let dns_resolver = Arc::new(TokioAsyncResolver::tokio_from_system_conf().unwrap()); + let mut conf = Configuration::new(); + conf.base_path = GRAFANA_BASE_PATH().to_string(); + conf.basic_auth = Some(( + GRAFANA_CLIENT_LOGIN().to_string(), + Some(GRAFANA_CLIENT_PASSWORD().to_string()), + )); + + let grafana_client_conf = Arc::new(conf); + // Passkey let rp_id = match ENVIRONMENT() { "DEV" => "localhost", @@ -61,6 +72,7 @@ impl CloudState { mailer, dns_resolver, webauthn, + grafana_client_conf, } } } diff --git a/server/src/env.rs b/server/src/env.rs index 890a9443..ba4a4501 100644 --- a/server/src/env.rs +++ b/server/src/env.rs @@ -10,6 +10,9 @@ pub struct ENV { pub NONCE: String, pub MAILER_ADDRESS: String, pub MAILER_PASSWORD: String, + pub GRAFANA_BASE_PATH: String, + pub GRAFANA_CLIENT_LOGIN: String, + pub GRAFANA_CLIENT_PASSWORD: String, } pub fn get_env() -> &'static ENV { static INSTANCE: OnceCell = OnceCell::new(); @@ -31,6 +34,12 @@ pub fn get_env() -> &'static ENV { .expect("Failed to get MAILER_ADDRESS env"), MAILER_PASSWORD: std::env::var("MAILER_PASSWORD") .expect("Failed to get MAILER_PASSWORD env"), + GRAFANA_BASE_PATH: std::env::var("GRAFANA_BASE_PATH") + .expect("Failed to get GRAFANA_BASE_PATH env"), + GRAFANA_CLIENT_LOGIN: std::env::var("GRAFANA_CLIENT_LOGIN") + .expect("Failed to get GRAFANA_CLIENT_LOGIN env"), + GRAFANA_CLIENT_PASSWORD: std::env::var("GRAFANA_CLIENT_PASSWORD") + .expect("Failed to get GRAFANA_CLIENT_PASSWORD env"), }; return env; }) @@ -60,3 +69,12 @@ pub fn MAILER_ADDRESS() -> &'static str { pub fn MAILER_PASSWORD() -> &'static str { get_env().MAILER_PASSWORD.as_str() } +pub fn GRAFANA_BASE_PATH() -> &'static str { + get_env().GRAFANA_BASE_PATH.as_str() +} +pub fn GRAFANA_CLIENT_LOGIN() -> &'static str { + get_env().GRAFANA_CLIENT_LOGIN.as_str() +} +pub fn GRAFANA_CLIENT_PASSWORD() -> &'static str { + get_env().GRAFANA_CLIENT_PASSWORD.as_str() +} diff --git a/server/src/http/cloud/accept_team_invite.rs b/server/src/http/cloud/accept_team_invite.rs index 23dc7ca3..3713ffc6 100644 --- a/server/src/http/cloud/accept_team_invite.rs +++ b/server/src/http/cloud/accept_team_invite.rs @@ -5,11 +5,15 @@ use axum::{extract::State, http::StatusCode, Extension, Json}; use database::db::Db; use garde::Validate; use log::error; +use openapi::apis::configuration::Configuration; use serde::{Deserialize, Serialize}; use std::sync::Arc; use ts_rs::TS; -use super::utils::{custom_validate_uuid, validate_request}; +use super::{ + grafana_utils::add_user_to_team::handle_grafana_add_user_to_team, + utils::{custom_validate_uuid, validate_request}, +}; #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, TS, Validate)] #[ts(export)] @@ -25,6 +29,7 @@ pub struct HttpAcceptTeamInviteResponse {} pub async fn accept_team_invite( State(db): State>, + State(grafana_conf): State>, Extension(user_id): Extension, Json(request): Json, ) -> Result, (StatusCode, String)> { @@ -96,6 +101,9 @@ pub async fn accept_team_invite( } } + // Grafana add user to the team + handle_grafana_add_user_to_team(&grafana_conf, &request.team_id, &user.email).await?; + // Accept invite let mut tx = match db.connection_pool.begin().await { Ok(tx) => tx, diff --git a/server/src/http/cloud/grafana_utils/add_user_to_team.rs b/server/src/http/cloud/grafana_utils/add_user_to_team.rs new file mode 100644 index 00000000..ac2f093e --- /dev/null +++ b/server/src/http/cloud/grafana_utils/add_user_to_team.rs @@ -0,0 +1,104 @@ +use crate::structs::cloud::{ + api_cloud_errors::CloudApiErrors, grafana_error::handle_grafana_error, +}; +use axum::http::StatusCode; +use log::{error, warn}; +use openapi::{ + apis::{ + admin_users_api::admin_create_user, + configuration::Configuration, + teams_api::add_team_member, + users_api::{get_user_by_login_or_email, get_user_teams}, + }, + models::{AddTeamMemberCommand, AdminCreateUserForm}, +}; +use rand::{distributions::Alphanumeric, thread_rng, Rng}; +use std::sync::Arc; + +pub async fn handle_grafana_add_user_to_team( + grafana_conf: &Arc, + team_id: &String, + user_email: &String, +) -> Result<(), (StatusCode, String)> { + // Check if user exists, if not create a new user + let user_id = match get_user_by_login_or_email(&grafana_conf, user_email).await { + Ok(user) => user.id, + Err(_) => { + // Create user with the same email as the user, password can be anything, it won't be used + let random_password: String = thread_rng() + .sample_iter(&Alphanumeric) + .take(30) + .map(char::from) + .collect(); + + let request = AdminCreateUserForm { + password: Some(random_password), + email: Some(user_email.clone()), + login: None, + name: None, + org_id: None, + }; + + match admin_create_user(&grafana_conf, request).await { + Ok(user) => user.id, + Err(err) => { + warn!("Failed to create user: {:?}", err); + return Err(( + StatusCode::INTERNAL_SERVER_ERROR, + CloudApiErrors::InternalServerError.to_string(), + )); + } + } + } + }; + + // If for some reason user_id is not found, return error + let id = match user_id { + Some(id) => id, + None => { + error!("Failed to get user_id for email: {:?}", user_email); + return Err(( + StatusCode::INTERNAL_SERVER_ERROR, + CloudApiErrors::InternalServerError.to_string(), + )); + } + }; + + // Check if user is already in the team + match get_user_teams(&grafana_conf, id.clone()).await { + Ok(teams) => { + let team_id: i64 = team_id.parse().map_err(|err| { + error!("Failed to parse team_id: {:?}", err); + ( + StatusCode::INTERNAL_SERVER_ERROR, + CloudApiErrors::InternalServerError.to_string(), + ) + })?; + + // For now we will be checking team id but in the future we might need to swap to team uid + if teams.iter().any(|team| team.id == Some(team_id)) { + return Err(( + StatusCode::BAD_REQUEST, + CloudApiErrors::UserAlreadyBelongsToTheTeam.to_string(), + )); + } + } + Err(err) => { + warn!("Failed to get user teams: {:?}", err); + return Err(handle_grafana_error(err)); + } + } + + // Add user to the team + let request = AddTeamMemberCommand { user_id: user_id }; + + if let Err(err) = add_team_member(&grafana_conf, team_id, request).await { + warn!( + "Failed to add user [{:?}] to team [{:?}], error: {:?}", + user_email, team_id, err + ); + return Err(handle_grafana_error(err)); + } + + Ok(()) +} diff --git a/server/src/http/cloud/grafana_utils/create_new_app.rs b/server/src/http/cloud/grafana_utils/create_new_app.rs new file mode 100644 index 00000000..32f7a3bf --- /dev/null +++ b/server/src/http/cloud/grafana_utils/create_new_app.rs @@ -0,0 +1,88 @@ +use crate::{ + statics::DASHBOARD_TEMPLATE_UID, + structs::cloud::{api_cloud_errors::CloudApiErrors, grafana_error::handle_grafana_error}, +}; +use axum::http::StatusCode; +use log::warn; +use openapi::{ + apis::{ + configuration::Configuration, + dashboards_api::{get_dashboard_by_uid, import_dashboard}, + }, + models::ImportDashboardRequest, +}; +use serde_json::json; +use std::sync::Arc; + +pub async fn handle_grafana_create_new_app( + grafana_conf: &Arc, + app_name: &String, + app_id: &String, + team_id: &String, +) -> Result<(), (StatusCode, String)> { + // Import template dashboard + let mut template_dashboard = + match get_dashboard_by_uid(&grafana_conf, &DASHBOARD_TEMPLATE_UID).await { + Ok(response) => match response.dashboard { + Some(dashboard) => dashboard, + None => { + warn!("Failed to get template dashboard: {:?}", response); + return Err(( + StatusCode::INTERNAL_SERVER_ERROR, + CloudApiErrors::DashboardImportFail.to_string(), + )); + } + }, + Err(err) => { + return Err(handle_grafana_error(err)); + } + }; + + // Modify dashboard template fields + if let Some(uid_field) = template_dashboard.get_mut("uid") { + *uid_field = json!(app_id); + } else { + return Err(( + StatusCode::INTERNAL_SERVER_ERROR, + CloudApiErrors::DashboardImportFail.to_string(), + )); + } + + if let Some(id_field) = template_dashboard.get_mut("id") { + *id_field = json!(""); // Set dashboard id to empty string to create a new dashboard + } else { + return Err(( + StatusCode::INTERNAL_SERVER_ERROR, + CloudApiErrors::DashboardImportFail.to_string(), + )); + } + + if let Some(title_field) = template_dashboard.get_mut("title") { + *title_field = json!(app_name); + } else { + return Err(( + StatusCode::INTERNAL_SERVER_ERROR, + CloudApiErrors::DashboardImportFail.to_string(), + )); + } + + // Import dashboard for the team + if let Err(err) = import_dashboard( + &grafana_conf, + ImportDashboardRequest { + dashboard: Some(template_dashboard), + folder_id: None, + folder_uid: Some(team_id.clone()), // When we create a new team, we create a folder with the same uid as the team id + inputs: None, + overwrite: Some(false), + path: None, + plugin_id: None, + }, + ) + .await + { + return Err(handle_grafana_error(err)); + }; + + Ok(()) +} diff --git a/server/src/http/cloud/grafana_utils/create_new_team.rs b/server/src/http/cloud/grafana_utils/create_new_team.rs new file mode 100644 index 00000000..21b8d0d6 --- /dev/null +++ b/server/src/http/cloud/grafana_utils/create_new_team.rs @@ -0,0 +1,87 @@ +use crate::structs::cloud::{ + api_cloud_errors::CloudApiErrors, grafana_error::handle_grafana_error, +}; +use axum::http::StatusCode; +use log::warn; +use openapi::{ + apis::{ + configuration::Configuration, folder_permissions_api::update_folder_permissions, + folders_api::create_folder, teams_api::create_team, + }, + models::{ + CreateFolderCommand, CreateTeamCommand, DashboardAclUpdateItem, UpdateDashboardAclCommand, + }, +}; +use std::sync::Arc; + +pub async fn handle_grafana_create_new_team( + grafana_conf: &Arc, + admin_email: &String, + team_name: &String, +) -> Result { + let grafana_team_name = format!("[{}][{}]", team_name, admin_email); + + // create new team + let team_request = CreateTeamCommand { + email: Some(admin_email.clone()), + name: Some(grafana_team_name.clone()), + }; + + let grafana_team_id = match create_team(&grafana_conf, team_request).await { + Ok(response) => match response.team_id { + Some(team_id) => team_id, + None => { + warn!("Failed to create team: {:?}", response); + return Err(( + StatusCode::INTERNAL_SERVER_ERROR, + CloudApiErrors::FailedToCreateTeam.to_string(), + )); + } + }, + Err(err) => { + return Err(handle_grafana_error(err)); + } + }; + + // create folder for team dashboards + let folder_request = CreateFolderCommand { + description: None, + parent_uid: None, + title: Some(grafana_team_name.clone()), + uid: Some(grafana_team_id.to_string()), + }; + + let folder_uid = match create_folder(&grafana_conf, folder_request).await { + Ok(response) => match response.uid { + Some(folder_uid) => folder_uid, + None => { + warn!("Failed to create folder: {:?}", response); + return Err(( + StatusCode::INTERNAL_SERVER_ERROR, + CloudApiErrors::FailedToCreateTeam.to_string(), + )); + } + }, + Err(err) => { + return Err(handle_grafana_error(err)); + } + }; + + // set folder permissions for the whole team + let update_permissions_request = UpdateDashboardAclCommand { + items: Some(vec![DashboardAclUpdateItem { + permission: Some(1), // Grant View permission for the whole team + role: None, + team_id: Some(grafana_team_id), + user_id: None, + }]), + }; + + if let Err(err) = + update_folder_permissions(&grafana_conf, &folder_uid, update_permissions_request).await + { + return Err(handle_grafana_error(err)); + } + + Ok(grafana_team_id) +} diff --git a/server/src/http/cloud/grafana_utils/mod.rs b/server/src/http/cloud/grafana_utils/mod.rs new file mode 100644 index 00000000..3fb8f18d --- /dev/null +++ b/server/src/http/cloud/grafana_utils/mod.rs @@ -0,0 +1,4 @@ +pub mod add_user_to_team; +pub mod create_new_app; +pub mod create_new_team; +pub mod remove_user_from_the_team; diff --git a/server/src/http/cloud/grafana_utils/remove_user_from_the_team.rs b/server/src/http/cloud/grafana_utils/remove_user_from_the_team.rs new file mode 100644 index 00000000..41431f8a --- /dev/null +++ b/server/src/http/cloud/grafana_utils/remove_user_from_the_team.rs @@ -0,0 +1,74 @@ +use crate::structs::cloud::{ + api_cloud_errors::CloudApiErrors, grafana_error::handle_grafana_error, +}; +use axum::http::StatusCode; +use log::{error, warn}; +use openapi::apis::{ + configuration::Configuration, + teams_api::remove_team_member, + users_api::{get_user_by_login_or_email, get_user_teams}, +}; +use std::sync::Arc; + +pub async fn handle_grafana_remove_user_from_team( + grafana_conf: &Arc, + team_id: &String, + user_email: &String, +) -> Result<(), (StatusCode, String)> { + // Check if user exists + let user_id = match get_user_by_login_or_email(&grafana_conf, user_email).await { + Ok(user) => user.id, + Err(err) => { + warn!("Failed to get user: {:?}", err); + return Err(handle_grafana_error(err)); + } + }; + + // If for some reason user_id is not found, return error + let id = match user_id { + Some(id) => id, + None => { + error!("Failed to get user_id for email: {:?}", user_email); + return Err(( + StatusCode::INTERNAL_SERVER_ERROR, + CloudApiErrors::InternalServerError.to_string(), + )); + } + }; + + // Check if user is already in the team + match get_user_teams(&grafana_conf, id.clone()).await { + Ok(teams) => { + let team_id: i64 = team_id.parse().map_err(|err| { + error!("Failed to parse team_id: {:?}", err); + ( + StatusCode::INTERNAL_SERVER_ERROR, + CloudApiErrors::InternalServerError.to_string(), + ) + })?; + + // For now we will be checking team id but in the future we might need to swap to team uid + if !teams.iter().any(|team| team.id == Some(team_id)) { + return Err(( + StatusCode::BAD_REQUEST, + CloudApiErrors::UserDoesNotBelongsToTheTeam.to_string(), + )); + } + } + Err(err) => { + warn!("Failed to get user teams: {:?}", err); + return Err(handle_grafana_error(err)); + } + } + + // Remove user from the team + if let Err(err) = remove_team_member(&grafana_conf, team_id, id).await { + warn!( + "Failed to add user [{:?}] to team [{:?}], error: {:?}", + user_email, team_id, err + ); + return Err(handle_grafana_error(err)); + } + + Ok(()) +} diff --git a/server/src/http/cloud/mod.rs b/server/src/http/cloud/mod.rs index 260cb917..3256cb85 100644 --- a/server/src/http/cloud/mod.rs +++ b/server/src/http/cloud/mod.rs @@ -15,6 +15,7 @@ pub mod get_team_users_privileges; pub mod get_user_joined_teams; pub mod get_user_metadata; pub mod get_user_team_invites; +pub mod grafana_utils; pub mod invite_user_to_team; pub mod login; pub mod register; diff --git a/server/src/http/cloud/register_new_app.rs b/server/src/http/cloud/register_new_app.rs index 89c49e5a..97e1a000 100644 --- a/server/src/http/cloud/register_new_app.rs +++ b/server/src/http/cloud/register_new_app.rs @@ -1,4 +1,7 @@ -use super::utils::{custom_validate_name, custom_validate_uuid, validate_request}; +use super::{ + grafana_utils::create_new_app::handle_grafana_create_new_app, + utils::{custom_validate_name, custom_validate_uuid, validate_request}, +}; use crate::{ middlewares::auth_middleware::UserId, statics::REGISTERED_APPS_LIMIT_PER_TEAM, structs::cloud::api_cloud_errors::CloudApiErrors, @@ -9,6 +12,7 @@ use database::{ }; use garde::Validate; use log::error; +use openapi::apis::configuration::Configuration; use serde::{Deserialize, Serialize}; use std::sync::Arc; use ts_rs::TS; @@ -33,6 +37,7 @@ pub struct HttpRegisterNewAppResponse { pub async fn register_new_app( State(db): State>, + State(grafana_conf): State>, Extension(user_id): Extension, Json(request): Json, ) -> Result, (StatusCode, String)> { @@ -94,12 +99,18 @@ pub async fn register_new_app( } } + // Generate a new app id + let app_id = uuid7().to_string(); + + // Grafana, add new app + handle_grafana_create_new_app(&grafana_conf, &request.app_name, &app_id, &team.team_id) + .await?; + // Register a new app under this team // Start a transaction let mut tx = db.connection_pool.begin().await.unwrap(); // Register a new app - let app_id = uuid7().to_string(); let db_registered_app = database::tables::registered_app::table_struct::DbRegisteredApp { app_id: app_id.clone(), diff --git a/server/src/http/cloud/register_new_team.rs b/server/src/http/cloud/register_new_team.rs index 6b2adcc1..493f07a9 100644 --- a/server/src/http/cloud/register_new_team.rs +++ b/server/src/http/cloud/register_new_team.rs @@ -1,3 +1,7 @@ +use super::{ + grafana_utils::create_new_team::handle_grafana_create_new_team, + utils::{custom_validate_name, validate_request}, +}; use crate::{ middlewares::auth_middleware::UserId, statics::TEAMS_AMOUNT_LIMIT_PER_USER, structs::cloud::api_cloud_errors::CloudApiErrors, @@ -9,13 +13,12 @@ use database::{ }; use garde::Validate; use log::error; +use openapi::apis::configuration::Configuration; use serde::{Deserialize, Serialize}; use std::sync::Arc; use ts_rs::TS; use uuid7::uuid7; -use super::utils::{custom_validate_name, validate_request}; - #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, TS, Validate)] #[ts(export)] #[serde(rename_all = "camelCase")] @@ -35,6 +38,7 @@ pub struct HttpRegisterNewTeamResponse { pub async fn register_new_team( State(db): State>, + State(grafana_conf): State>, Extension(user_id): Extension, Json(request): Json, ) -> Result, (StatusCode, String)> { @@ -98,10 +102,32 @@ pub async fn register_new_team( } } + // Get team admin email + let admin_email = match db.get_user_by_user_id(&user_id).await { + Ok(Some(user)) => user.email, + Ok(None) => { + return Err(( + StatusCode::INTERNAL_SERVER_ERROR, + CloudApiErrors::DatabaseError.to_string(), + )); + } + Err(err) => { + error!("Failed to get user by user_id: {:?}", err); + return Err(( + StatusCode::INTERNAL_SERVER_ERROR, + CloudApiErrors::DatabaseError.to_string(), + )); + } + }; + + // Grafana, add new team + let grafana_team_id = + handle_grafana_create_new_team(&grafana_conf, &admin_email, &user_id).await?; + // Create a new team let team_id = uuid7().to_string(); let team = Team { - team_id: team_id.clone(), + team_id: grafana_team_id.to_string(), team_name: request.team_name.clone(), team_admin_id: user_id.clone(), subscription: None, diff --git a/server/src/http/cloud/remove_user_from_team.rs b/server/src/http/cloud/remove_user_from_team.rs index 76514dee..95e7a524 100644 --- a/server/src/http/cloud/remove_user_from_team.rs +++ b/server/src/http/cloud/remove_user_from_team.rs @@ -11,11 +11,15 @@ use axum::{extract::State, http::StatusCode, Extension, Json}; use database::db::Db; use garde::Validate; use log::error; +use openapi::apis::configuration::Configuration; use serde::{Deserialize, Serialize}; use std::sync::Arc; use ts_rs::TS; -use super::utils::{custom_validate_uuid, validate_request}; +use super::{ + grafana_utils::remove_user_from_the_team::handle_grafana_remove_user_from_team, + utils::{custom_validate_uuid, validate_request}, +}; #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, TS, Validate)] #[ts(export)] @@ -33,6 +37,7 @@ pub struct HttpRemoveUserFromTeamResponse {} pub async fn remove_user_from_team( State(db): State>, + State(grafana_conf): State>, State(mailer): State>, Extension(user_id): Extension, Json(request): Json, @@ -95,6 +100,14 @@ pub async fn remove_user_from_team( } } + // Grafana, remove user from the team + handle_grafana_remove_user_from_team( + &grafana_conf, + &request.team_id, + &request.user_email, + ) + .await?; + // Remove user from the team if let Err(err) = db .remove_user_from_the_team(&user.user_id, &request.team_id) diff --git a/server/src/state.rs b/server/src/state.rs index b7fe77e7..87cd4c91 100644 --- a/server/src/state.rs +++ b/server/src/state.rs @@ -16,6 +16,7 @@ use axum::extract::{ use database::db::Db; use futures::{stream::SplitSink, SinkExt}; use log::info; +use openapi::apis::configuration::Configuration; use std::{ collections::{HashMap, HashSet}, sync::Arc, @@ -75,6 +76,17 @@ impl FromRef for Arc { state.cloud_state.as_ref().unwrap().webauthn.clone() } } +impl FromRef for Arc { + fn from_ref(state: &ServerState) -> Self { + // Safe as middleware will prevent this from being None + state + .cloud_state + .as_ref() + .unwrap() + .grafana_client_conf + .clone() + } +} #[async_trait] pub trait DisconnectUser { diff --git a/server/src/statics.rs b/server/src/statics.rs index fecc6406..4435554d 100644 --- a/server/src/statics.rs +++ b/server/src/statics.rs @@ -5,6 +5,8 @@ pub const TEAMS_AMOUNT_LIMIT_PER_USER: usize = 10; pub const REGISTERED_APPS_LIMIT_PER_TEAM: usize = 20; pub const USERS_AMOUNT_LIMIT_PER_TEAM: usize = 50; +pub const DASHBOARD_TEMPLATE_UID: &str = "TEMPLATE_UID"; + // Name must be 3-30 characters long and include only alphanumeric characters, underscores, or slashes. pub static NAME_REGEX: Lazy = Lazy::new(|| Regex::new(r"^[a-zA-Z0-9_-]{3,30}$").expect("Regex creation failed")); diff --git a/server/src/structs/cloud/api_cloud_errors.rs b/server/src/structs/cloud/api_cloud_errors.rs index 7852a592..806c83ab 100644 --- a/server/src/structs/cloud/api_cloud_errors.rs +++ b/server/src/structs/cloud/api_cloud_errors.rs @@ -44,4 +44,6 @@ pub enum CloudApiErrors { PasskeyAlreadyExists, InvalidPasskeyCredential, PasskeyDoesNotExist, + FailedToCreateTeam, + DashboardImportFail, } diff --git a/server/src/structs/cloud/grafana_error.rs b/server/src/structs/cloud/grafana_error.rs new file mode 100644 index 00000000..f939039b --- /dev/null +++ b/server/src/structs/cloud/grafana_error.rs @@ -0,0 +1,60 @@ +use axum::http::StatusCode; +use log::{info, warn}; +use openapi::{apis::Error, models::ErrorResponseBody}; +use serde::Serialize; +use std::fmt; + +pub fn handle_grafana_error(error: Error) -> (StatusCode, String) +where + T: Serialize + fmt::Debug, +{ + match error { + Error::Reqwest(err) => { + warn!("Network error: {}", err); + ( + StatusCode::BAD_GATEWAY, + "Network error occurred".to_string(), + ) + } + Error::Serde(err) => { + warn!("Serialization/deserialization error: {}", err); + ( + StatusCode::INTERNAL_SERVER_ERROR, + "Error processing data".to_string(), + ) + } + Error::Io(err) => { + warn!("I/O error: {}", err); + ( + StatusCode::INTERNAL_SERVER_ERROR, + "I/O error occurred".to_string(), + ) + } + Error::ResponseError(response_content) => { + info!( + "HTTP error with status {}: {}, entity: {:?}", + response_content.status, response_content.content, response_content.entity + ); + let message = match &response_content.entity { + Some(entity) => { + let serialized = + serde_json::to_string(entity).unwrap_or_else(|_| "{}".to_string()); + serde_json::from_str::(&serialized).map_or_else( + |_| { + warn!("Failed to extract ErrorResponseBody, using original content"); + response_content.content.clone() + }, + |body| body.message, + ) + } + None => response_content.content.clone(), + }; + let status_code = StatusCode::from_u16(response_content.status.as_u16()) + .unwrap_or_else(|_| { + warn!("Failed to convert status code: {}", response_content.status); + StatusCode::INTERNAL_SERVER_ERROR + }); + (status_code, message) + } + } +} diff --git a/server/src/structs/cloud/mod.rs b/server/src/structs/cloud/mod.rs index 8cf0bb28..1e8eadba 100644 --- a/server/src/structs/cloud/mod.rs +++ b/server/src/structs/cloud/mod.rs @@ -3,6 +3,7 @@ pub mod app_event; pub mod app_info; pub mod cloud_events; pub mod cloud_http_endpoints; +pub mod grafana_error; pub mod joined_team; pub mod new_user_privilege_level; pub mod team_invite;