From 1fbbbc5d7d00ca0127eb4efcb5fc13a13410da3a Mon Sep 17 00:00:00 2001 From: Brandon Thomas Date: Wed, 24 Sep 2025 19:11:10 -0400 Subject: [PATCH] generics, endpoint --- .../database/mysql_queries/src/errors/mod.rs | 3 +- .../mysql_queries/src/errors/mysql_error.rs | 26 +++++++ .../mysql_queries/src/errors/subtypes/mod.rs | 1 + .../upsert_error.rs} | 22 ++++-- .../src/queries/analytics_active_users/mod.rs | 2 +- .../upsert_analytics_app_active_user.rs | 8 +- .../common_responses/common_web_error.rs | 15 ++++ .../analytics/log_app_active_user_handler.rs | 73 +++++++++++++++++++ .../http_server/endpoints/analytics/mod.rs | 3 +- 9 files changed, 142 insertions(+), 11 deletions(-) create mode 100644 crates/schema/database/mysql_queries/src/errors/mysql_error.rs create mode 100644 crates/schema/database/mysql_queries/src/errors/subtypes/mod.rs rename crates/schema/database/mysql_queries/src/errors/{mysql_upsert_error.rs => subtypes/upsert_error.rs} (63%) create mode 100644 crates/service/web/storyteller_web/src/http_server/endpoints/analytics/log_app_active_user_handler.rs diff --git a/crates/schema/database/mysql_queries/src/errors/mod.rs b/crates/schema/database/mysql_queries/src/errors/mod.rs index 3065744ce4..f9e6852c0f 100644 --- a/crates/schema/database/mysql_queries/src/errors/mod.rs +++ b/crates/schema/database/mysql_queries/src/errors/mod.rs @@ -1,5 +1,6 @@ pub mod database_insert_error; pub mod database_query_error; +pub mod mysql_error; pub mod select_exactly_one_error; pub mod select_optional_record_error; -pub mod mysql_upsert_error; \ No newline at end of file +pub mod subtypes; \ No newline at end of file diff --git a/crates/schema/database/mysql_queries/src/errors/mysql_error.rs b/crates/schema/database/mysql_queries/src/errors/mysql_error.rs new file mode 100644 index 0000000000..9a81e6bf30 --- /dev/null +++ b/crates/schema/database/mysql_queries/src/errors/mysql_error.rs @@ -0,0 +1,26 @@ +use std::error::Error; +use std::fmt::{Debug, Display, Formatter}; + + +/// Implement this marker trait for subtypes. +pub trait CrateError : Error + Debug + Display {} + +#[derive(Debug)] +pub struct MysqlError { + /// Inner error type + pub inner: E +} + +impl Error for MysqlError where E: CrateError {} + +impl Display for MysqlError where E: CrateError { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + write!(f, "MysqlError: {}", self.inner) + } +} + +// impl From for MysqlError { +// fn from(value: UpsertError) -> Self { +// MysqlError { inner: value } +// } +// } diff --git a/crates/schema/database/mysql_queries/src/errors/subtypes/mod.rs b/crates/schema/database/mysql_queries/src/errors/subtypes/mod.rs new file mode 100644 index 0000000000..0a5de28d1d --- /dev/null +++ b/crates/schema/database/mysql_queries/src/errors/subtypes/mod.rs @@ -0,0 +1 @@ +pub mod upsert_error; \ No newline at end of file diff --git a/crates/schema/database/mysql_queries/src/errors/mysql_upsert_error.rs b/crates/schema/database/mysql_queries/src/errors/subtypes/upsert_error.rs similarity index 63% rename from crates/schema/database/mysql_queries/src/errors/mysql_upsert_error.rs rename to crates/schema/database/mysql_queries/src/errors/subtypes/upsert_error.rs index ab29773a58..ef7f9f07ed 100644 --- a/crates/schema/database/mysql_queries/src/errors/mysql_upsert_error.rs +++ b/crates/schema/database/mysql_queries/src/errors/subtypes/upsert_error.rs @@ -1,9 +1,10 @@ +use crate::errors::mysql_error::{CrateError, MysqlError}; use log::error; use std::error::Error; use std::fmt::{Display, Formatter}; #[derive(Debug)] -pub enum MysqlUpsertError { +pub enum UpsertError { /// A duplicate key error occurred. DuplicateKeyError(String), @@ -11,18 +12,20 @@ pub enum MysqlUpsertError { SqlxError(sqlx::Error), } -impl Error for MysqlUpsertError {} +impl Error for UpsertError {} -impl Display for MysqlUpsertError { +impl CrateError for UpsertError {} + +impl Display for UpsertError { fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { match self { - MysqlUpsertError::DuplicateKeyError(reason) => write!(f, "DuplicateKeyError: {}", reason), - MysqlUpsertError::SqlxError(e) => write!(f, "SQLx error: {:?}", e), + UpsertError::DuplicateKeyError(reason) => write!(f, "DuplicateKeyError: {}", reason), + UpsertError::SqlxError(e) => write!(f, "SQLx error: {:?}", e), } } } -impl From for MysqlUpsertError { +impl From for UpsertError { fn from(err: sqlx::Error) -> Self { if let Some(db_err) = err.as_database_error() { // NB: SQLSTATE[23000]: Integrity constraint violation @@ -39,3 +42,10 @@ impl From for MysqlUpsertError { Self::SqlxError(err) } } + +impl From for MysqlError { + fn from(err: sqlx::Error) -> Self { + let inner = UpsertError::from(err); + MysqlError { inner } + } +} diff --git a/crates/schema/database/mysql_queries/src/queries/analytics_active_users/mod.rs b/crates/schema/database/mysql_queries/src/queries/analytics_active_users/mod.rs index 0c873d7628..dc1f3e518a 100644 --- a/crates/schema/database/mysql_queries/src/queries/analytics_active_users/mod.rs +++ b/crates/schema/database/mysql_queries/src/queries/analytics_active_users/mod.rs @@ -1 +1 @@ -mod upsert_analytics_app_active_user; \ No newline at end of file +pub mod upsert_analytics_app_active_user; \ No newline at end of file diff --git a/crates/schema/database/mysql_queries/src/queries/analytics_active_users/upsert_analytics_app_active_user.rs b/crates/schema/database/mysql_queries/src/queries/analytics_active_users/upsert_analytics_app_active_user.rs index 3dac8797dd..e68c726da3 100644 --- a/crates/schema/database/mysql_queries/src/queries/analytics_active_users/upsert_analytics_app_active_user.rs +++ b/crates/schema/database/mysql_queries/src/queries/analytics_active_users/upsert_analytics_app_active_user.rs @@ -1,4 +1,5 @@ -use crate::errors::mysql_upsert_error::MysqlUpsertError; +use crate::errors::mysql_error::MysqlError; +use crate::errors::subtypes::upsert_error::UpsertError; use enums::common::payments_namespace::PaymentsNamespace; use sqlx::mysql::MySqlArguments; use sqlx::pool::PoolConnection; @@ -44,7 +45,10 @@ ON DUPLICATE KEY UPDATE ) } - pub async fn upsert_with_connection(&self, mysql_connection: &mut PoolConnection) -> Result<(), MysqlUpsertError> { + pub async fn upsert_with_connection( + &self, + mysql_connection: &mut PoolConnection + ) -> Result<(), MysqlError> { let _query_result = self.query() .execute(&mut **mysql_connection) .await?; diff --git a/crates/service/web/storyteller_web/src/http_server/common_responses/common_web_error.rs b/crates/service/web/storyteller_web/src/http_server/common_responses/common_web_error.rs index 99c7657a89..9ff1326eed 100644 --- a/crates/service/web/storyteller_web/src/http_server/common_responses/common_web_error.rs +++ b/crates/service/web/storyteller_web/src/http_server/common_responses/common_web_error.rs @@ -3,6 +3,7 @@ use actix_web::{HttpResponse, HttpResponseBuilder, ResponseError}; use std::error::Error; use std::fmt::{Display, Formatter}; use log::error; +use mysql_queries::errors::mysql_error::{CrateError, MysqlError}; #[derive(Debug)] pub enum CommonWebError { @@ -84,6 +85,20 @@ impl From for CommonWebError { } } +impl From for CommonWebError { + fn from(value: anyhow::Error) -> Self { + error!("Uncaught anyhow::Error being coerced to CommonWebError : {:?}", value); + CommonWebError::ServerError + } +} + +impl From> for CommonWebError { + fn from(value: MysqlError) -> Self { + error!("MysqlError being coerced to CommonWebError : {:?}", value); + CommonWebError::ServerError + } +} + #[cfg(test)] mod tests { use super::*; diff --git a/crates/service/web/storyteller_web/src/http_server/endpoints/analytics/log_app_active_user_handler.rs b/crates/service/web/storyteller_web/src/http_server/endpoints/analytics/log_app_active_user_handler.rs new file mode 100644 index 0000000000..39a4a87601 --- /dev/null +++ b/crates/service/web/storyteller_web/src/http_server/endpoints/analytics/log_app_active_user_handler.rs @@ -0,0 +1,73 @@ +use std::sync::Arc; + +use crate::http_server::common_responses::common_web_error::CommonWebError; +use crate::state::server_state::ServerState; +use actix_web::web::Json; +use actix_web::{web, HttpRequest}; +use enums::common::payments_namespace::PaymentsNamespace; +use http_server_common::request::get_request_ip::get_request_ip; +use mysql_queries::queries::analytics_active_users::upsert_analytics_app_active_user::UpsertAnalyticsAppActiveUser; +use utoipa::ToSchema; + +#[derive(Deserialize, ToSchema)] +pub struct LogAppActiveUserRequest { + /// An override for the application version. + maybe_app_version: Option, +} + +#[derive(Serialize, ToSchema)] +pub struct LogAppActiveUserResponse { + pub success: bool, +} + +/// Log an active app user - user must be logged in. +#[utoipa::path( + post, + tag = "Analytics", + path = "/v1/analytics/active_user", + responses( + (status = 200, description = "Success", body = LogAppActiveUserSuccessResponse), + ), + params( + ("request" = LogAppActiveUserRequest, description = "Payload for Request"), + ) +)] +pub async fn log_browser_session_handler( + http_request: HttpRequest, + request: Json, + server_state: web::Data>, +) -> Result, CommonWebError> +{ + let mut mysql_connection = server_state.mysql_pool + .acquire() + .await?; + + let maybe_user_session = server_state + .session_checker + .maybe_get_user_session_from_connection(&http_request, &mut mysql_connection) + .await?; + + let user_token = maybe_user_session + .ok_or(CommonWebError::NotAuthorized)? + .user_token; + + let ip_address = get_request_ip(&http_request); + + let app_version = request.maybe_app_version + .as_deref() + .unwrap_or("unknown"); + + let upsert = UpsertAnalyticsAppActiveUser { + namespace: PaymentsNamespace::Artcraft, + user_token: &user_token, + ip_address: &ip_address, + app_version, + }; + + upsert.upsert_with_connection(&mut mysql_connection).await?; + + Ok(Json(LogAppActiveUserResponse { + success: true, + })) +} + diff --git a/crates/service/web/storyteller_web/src/http_server/endpoints/analytics/mod.rs b/crates/service/web/storyteller_web/src/http_server/endpoints/analytics/mod.rs index cdaa741f37..4e84a818b1 100644 --- a/crates/service/web/storyteller_web/src/http_server/endpoints/analytics/mod.rs +++ b/crates/service/web/storyteller_web/src/http_server/endpoints/analytics/mod.rs @@ -1 +1,2 @@ -pub mod log_browser_session_handler; \ No newline at end of file +pub mod log_browser_session_handler; +pub mod log_app_active_user_handler; \ No newline at end of file