diff --git a/crates/schema/public/enums/src/by_table/staff_audit_logs/staff_audit_action.rs b/crates/schema/public/enums/src/by_table/staff_audit_logs/staff_audit_action.rs index f86252345c..e4be38e262 100644 --- a/crates/schema/public/enums/src/by_table/staff_audit_logs/staff_audit_action.rs +++ b/crates/schema/public/enums/src/by_table/staff_audit_logs/staff_audit_action.rs @@ -36,6 +36,10 @@ pub enum StaffAuditAction { /// Staff sent a manual alert via the pager system. #[serde(rename = "send_alert")] SendAlert, + + /// Staff edited a user's feature flags. + #[serde(rename = "edit_user_feature_flags")] + EditUserFeatureFlags, } impl_enum_display_and_debug_using_to_str!(StaffAuditAction); @@ -51,6 +55,7 @@ impl StaffAuditAction { Self::UnbanUser => "unban_user", Self::AddWalletBankedBalance => "add_wallet_banked_balance", Self::SendAlert => "send_alert", + Self::EditUserFeatureFlags => "edit_user_feature_flags", } } @@ -62,6 +67,7 @@ impl StaffAuditAction { "unban_user" => Ok(Self::UnbanUser), "add_wallet_banked_balance" => Ok(Self::AddWalletBankedBalance), "send_alert" => Ok(Self::SendAlert), + "edit_user_feature_flags" => Ok(Self::EditUserFeatureFlags), _ => Err(format!("invalid StaffAuditAction value: {:?}", value)), } } @@ -74,6 +80,7 @@ impl StaffAuditAction { Self::UnbanUser, Self::AddWalletBankedBalance, Self::SendAlert, + Self::EditUserFeatureFlags, ]) } } @@ -94,6 +101,7 @@ mod tests { assert_serialization(StaffAuditAction::UnbanUser, "unban_user"); assert_serialization(StaffAuditAction::AddWalletBankedBalance, "add_wallet_banked_balance"); assert_serialization(StaffAuditAction::SendAlert, "send_alert"); + assert_serialization(StaffAuditAction::EditUserFeatureFlags, "edit_user_feature_flags"); } #[test] @@ -104,6 +112,7 @@ mod tests { assert_eq!(StaffAuditAction::UnbanUser.to_str(), "unban_user"); assert_eq!(StaffAuditAction::AddWalletBankedBalance.to_str(), "add_wallet_banked_balance"); assert_eq!(StaffAuditAction::SendAlert.to_str(), "send_alert"); + assert_eq!(StaffAuditAction::EditUserFeatureFlags.to_str(), "edit_user_feature_flags"); } #[test] @@ -114,12 +123,13 @@ mod tests { assert_eq!(StaffAuditAction::from_str("unban_user").unwrap(), StaffAuditAction::UnbanUser); assert_eq!(StaffAuditAction::from_str("add_wallet_banked_balance").unwrap(), StaffAuditAction::AddWalletBankedBalance); assert_eq!(StaffAuditAction::from_str("send_alert").unwrap(), StaffAuditAction::SendAlert); + assert_eq!(StaffAuditAction::from_str("edit_user_feature_flags").unwrap(), StaffAuditAction::EditUserFeatureFlags); assert!(StaffAuditAction::from_str("invalid").is_err()); } #[test] fn all_variants() { - const EXPECTED_COUNT: usize = 6; + const EXPECTED_COUNT: usize = 7; assert_eq!(StaffAuditAction::all_variants().len(), EXPECTED_COUNT); } } diff --git a/crates/service/web/storyteller_web/src/http_server/endpoints/moderation/user_feature_flags/moderator_edit_user_feature_flags_handler.rs b/crates/service/web/storyteller_web/src/http_server/endpoints/moderation/user_feature_flags/moderator_edit_user_feature_flags_handler.rs index 5615d03dcb..3b6846b08b 100644 --- a/crates/service/web/storyteller_web/src/http_server/endpoints/moderation/user_feature_flags/moderator_edit_user_feature_flags_handler.rs +++ b/crates/service/web/storyteller_web/src/http_server/endpoints/moderation/user_feature_flags/moderator_edit_user_feature_flags_handler.rs @@ -1,6 +1,7 @@ use std::collections::BTreeSet; use std::fmt; use std::iter::FromIterator; +use std::marker::PhantomData; use std::sync::Arc; use actix_web::error::ResponseError; @@ -9,10 +10,14 @@ use actix_web::web::{Data, Json, Path}; use actix_web::{HttpRequest, HttpResponse}; use log::warn; use redis::{Client, Commands}; +use sqlx::Acquire; use utoipa::ToSchema; +use enums::by_table::staff_audit_logs::staff_audit_action::StaffAuditAction; +use enums::by_table::staff_audit_logs::staff_audit_entity_type::StaffAuditEntityType; use enums::by_table::users::user_feature_flag::UserFeatureFlag; use http_server_common::request::get_request_ip::get_request_ip; +use mysql_queries::queries::staff_audit_logs::insert_staff_audit_log::{insert_staff_audit_log, InsertStaffAuditLogArgs}; use mysql_queries::queries::users::user::get::get_user_token_by_username::get_user_token_by_username; use mysql_queries::queries::users::user::update::set_user_feature_flags::{set_user_feature_flags, SetUserFeatureFlagArgs}; use mysql_queries::queries::users::user_profiles::get_user_profile_by_token::get_user_profile_by_token; @@ -114,7 +119,6 @@ pub async fn moderator_edit_user_feature_flags_handler( path: Path, request: Json, server_state: Data>, - //redis_ttl_cache: Data, redis_pool: Data>, ) -> Result { @@ -129,34 +133,28 @@ pub async fn moderator_edit_user_feature_flags_handler( let username_or_token = path.username_or_token.trim(); - let user_token; - - if username_or_token.starts_with(UserToken::token_prefix()) || username_or_token.starts_with("U:") { - user_token = UserToken::new_from_str(username_or_token); + let user_token = if username_or_token.starts_with(UserToken::token_prefix()) || username_or_token.starts_with("U:") { + UserToken::new_from_str(username_or_token) } else { - user_token = get_user_token_by_username(&username_or_token, &server_state.mysql_pool) + get_user_token_by_username(username_or_token, &server_state.mysql_pool) .await .map_err(|e| { warn!("Could not get user token by username: {:?}", e); EditUserFeatureFlagsError::ServerError })? - .ok_or_else(|| { - EditUserFeatureFlagsError::ServerError - })?; - } + .ok_or(EditUserFeatureFlagsError::ServerError)? + }; let user_profile = get_user_profile_by_token(&user_token, &server_state.mysql_pool) .await .map_err(|e| { - warn!("Could not get user session by token: {:?}", e); + warn!("Could not get user profile by token: {:?}", e); EditUserFeatureFlagsError::ServerError })? - .ok_or_else(|| { - EditUserFeatureFlagsError::ServerError - })?; + .ok_or(EditUserFeatureFlagsError::ServerError)?; let mut user_feature_flags = - UserSessionFeatureFlags::new(user_profile.maybe_feature_flags.as_deref()); + UserSessionFeatureFlags::new(user_profile.maybe_feature_flags.as_deref()); match &request.action { EditUserFeatureFlagsOption::AddFlags { flags } => { @@ -180,6 +178,7 @@ pub async fn moderator_edit_user_feature_flags_handler( let ip_address = get_request_ip(&http_request); + // Update the user's feature flags. set_user_feature_flags(SetUserFeatureFlagArgs { subject_user_token: &user_profile.user_token, maybe_feature_flags: user_feature_flags.maybe_serialize_string().as_deref(), @@ -192,16 +191,45 @@ pub async fn moderator_edit_user_feature_flags_handler( EditUserFeatureFlagsError::ServerError })?; - //if let Ok(mut redis) = redis_ttl_cache.get_connection() { - // // TODO(bt,2024-04-20): This should be coordinated with other code. - // let cache_key = format!("cache:userProfile:{}", user_profile.username); - // let _r = redis.delete_from_cache(&cache_key); - //} + // Insert staff audit log. + let mut mysql_connection = server_state.mysql_pool.acquire() + .await + .map_err(|e| { + warn!("Could not acquire MySQL connection for audit log: {:?}", e); + EditUserFeatureFlagsError::ServerError + })?; + let mut transaction = mysql_connection.begin() + .await + .map_err(|e| { + warn!("Could not start transaction for audit log: {:?}", e); + EditUserFeatureFlagsError::ServerError + })?; + + let _audit_token = insert_staff_audit_log(InsertStaffAuditLogArgs { + audit_action: StaffAuditAction::EditUserFeatureFlags, + maybe_entity_type: Some(StaffAuditEntityType::User), + maybe_entity_token: Some(user_profile.user_token.as_str()), + staff_user_token: &user_session.user_token, + actor_ip_address: &ip_address, + mysql_executor: &mut *transaction, + phantom: PhantomData, + }).await.map_err(|err| { + warn!("Failed to insert staff audit log: {:?}", err); + EditUserFeatureFlagsError::ServerError + })?; + + transaction.commit() + .await + .map_err(|e| { + warn!("Could not commit audit log transaction: {:?}", e); + EditUserFeatureFlagsError::ServerError + })?; + + // Invalidate Redis cache for the user profile. if let Ok(mut redis) = redis_pool.get() { - // TODO(bt,2024-04-20): This should be coordinated with other code. let cache_key = format!("cache:userProfile:{}", user_profile.username); - let _r : Result, _> = redis.del(&cache_key); + let _r: Result, _> = redis.del(&cache_key); } Ok(simple_json_success()) diff --git a/crates/service/web/storyteller_web/src/http_server/endpoints/moderation/user_feature_flags/moderator_list_all_available_user_feature_flags_handler.rs b/crates/service/web/storyteller_web/src/http_server/endpoints/moderation/user_feature_flags/moderator_list_all_available_user_feature_flags_handler.rs index 28bace5ddf..2581f247fe 100644 --- a/crates/service/web/storyteller_web/src/http_server/endpoints/moderation/user_feature_flags/moderator_list_all_available_user_feature_flags_handler.rs +++ b/crates/service/web/storyteller_web/src/http_server/endpoints/moderation/user_feature_flags/moderator_list_all_available_user_feature_flags_handler.rs @@ -23,14 +23,14 @@ pub struct ModeratorListUserFeatureFlagsResponse { #[derive(Serialize, ToSchema)] pub struct FeatureFlagDescriptor { + /// Key the flag is identified by + pub key: String, + /// Full name of the flag pub full_name: String, /// Description of the flag pub description: String, - - /// Key the flag is identified by - pub key: String, } // ── Handler ── @@ -61,9 +61,9 @@ pub async fn moderator_list_all_available_user_feature_flags_handler( let feature_flags: Vec = UserFeatureFlag::all_variants() .into_iter() .map(|flag| FeatureFlagDescriptor { + key: flag.to_str().to_string(), full_name: flag.name().to_string(), description: flag.description().to_string(), - key: flag.to_str().to_string(), }) .collect(); diff --git a/crates/service/web/storyteller_web/src/http_server/endpoints/moderation/user_feature_flags/moderator_list_user_feature_flags_handler.rs b/crates/service/web/storyteller_web/src/http_server/endpoints/moderation/user_feature_flags/moderator_list_user_feature_flags_handler.rs index 4306b70e9b..76f0bdb4a1 100644 --- a/crates/service/web/storyteller_web/src/http_server/endpoints/moderation/user_feature_flags/moderator_list_user_feature_flags_handler.rs +++ b/crates/service/web/storyteller_web/src/http_server/endpoints/moderation/user_feature_flags/moderator_list_user_feature_flags_handler.rs @@ -31,6 +31,7 @@ pub struct ModeratorListUserFeatureFlagsResponse { pub success: bool, pub user_token: String, pub username: String, + pub display_name: String, pub feature_flags: Vec, } @@ -71,23 +72,23 @@ pub async fn moderator_list_user_feature_flags_handler( } else { get_user_token_by_username(username_or_token, &server_state.mysql_pool) .await - .map_err(|e| { - warn!("Could not get user token by username: {:?}", e); - AdvancedCommonWebError::server_error_with_message("Could not look up user") + .map_err(|err| { + warn!("Could not get user token by username: {:?}", err); + AdvancedCommonWebError::from_anyhow_error(err) })? .ok_or_else(|| { - AdvancedCommonWebError::BadInputWithSimpleMessage("User not found".to_string()) + AdvancedCommonWebError::NotFound })? }; let user_profile = get_user_profile_by_token(&user_token, &server_state.mysql_pool) .await - .map_err(|e| { - warn!("Could not get user profile by token: {:?}", e); - AdvancedCommonWebError::server_error_with_message("Could not look up user profile") + .map_err(|err| { + warn!("Could not get user profile by token: {:?}", err); + AdvancedCommonWebError::from_anyhow_error(err) })? .ok_or_else(|| { - AdvancedCommonWebError::BadInputWithSimpleMessage("User not found".to_string()) + AdvancedCommonWebError::NotFound })?; let user_feature_flags = @@ -102,6 +103,7 @@ pub async fn moderator_list_user_feature_flags_handler( success: true, user_token: user_profile.user_token.as_str().to_string(), username: user_profile.username, + display_name: user_profile.display_name, feature_flags: flags, })) }