diff --git a/.sqlx/query-74b401958826a7e640540f248114f57a941324787d023783f84251e8dd56eeb7.json b/.sqlx/query-74b401958826a7e640540f248114f57a941324787d023783f84251e8dd56eeb7.json new file mode 100644 index 0000000000..7288bf6a16 --- /dev/null +++ b/.sqlx/query-74b401958826a7e640540f248114f57a941324787d023783f84251e8dd56eeb7.json @@ -0,0 +1,157 @@ +{ + "db_name": "MySQL", + "query": "\nSELECT\n b.token as `token: tokens::tokens::beta_keys::BetaKeyToken`,\n\n b.product as `product: enums::by_table::beta_keys::beta_key_product::BetaKeyProduct`,\n b.key_value,\n\n b.maybe_referrer_user_token as `maybe_referrer_user_token: tokens::tokens::users::UserToken`,\n referrer.username as maybe_referrer_username,\n referrer.display_name as maybe_referrer_display_name,\n referrer.email_gravatar_hash as maybe_referrer_gravatar_hash,\n\n b.maybe_redeemer_user_token as `maybe_redeemer_user_token: tokens::tokens::users::UserToken`,\n redeemer.username as maybe_redeemer_username,\n redeemer.display_name as maybe_redeemer_display_name,\n redeemer.email_gravatar_hash as maybe_redeemer_gravatar_hash,\n\n b.created_at,\n b.maybe_redeemed_at\n\nFROM beta_keys AS b\n\nLEFT OUTER JOIN users AS referrer\n ON b.maybe_referrer_user_token = referrer.token\n\nLEFT OUTER JOIN users AS redeemer\n ON b.maybe_redeemer_user_token = redeemer.token\n\nWHERE\n b.key_value = ?\n ", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "token: tokens::tokens::beta_keys::BetaKeyToken", + "type_info": { + "type": "VarString", + "flags": "NOT_NULL | UNIQUE_KEY | BINARY | NO_DEFAULT_VALUE", + "char_set": 224, + "max_size": 128 + } + }, + { + "ordinal": 1, + "name": "product: enums::by_table::beta_keys::beta_key_product::BetaKeyProduct", + "type_info": { + "type": "VarString", + "flags": "NOT_NULL | BINARY | NO_DEFAULT_VALUE", + "char_set": 224, + "max_size": 128 + } + }, + { + "ordinal": 2, + "name": "key_value", + "type_info": { + "type": "VarString", + "flags": "NOT_NULL | UNIQUE_KEY | BINARY | NO_DEFAULT_VALUE", + "char_set": 224, + "max_size": 128 + } + }, + { + "ordinal": 3, + "name": "maybe_referrer_user_token: tokens::tokens::users::UserToken", + "type_info": { + "type": "VarString", + "flags": "MULTIPLE_KEY | BINARY", + "char_set": 224, + "max_size": 128 + } + }, + { + "ordinal": 4, + "name": "maybe_referrer_username", + "type_info": { + "type": "VarString", + "flags": "UNIQUE_KEY | BINARY | NO_DEFAULT_VALUE", + "char_set": 224, + "max_size": 80 + } + }, + { + "ordinal": 5, + "name": "maybe_referrer_display_name", + "type_info": { + "type": "VarString", + "flags": "BINARY | NO_DEFAULT_VALUE", + "char_set": 224, + "max_size": 80 + } + }, + { + "ordinal": 6, + "name": "maybe_referrer_gravatar_hash", + "type_info": { + "type": "String", + "flags": "BINARY | NO_DEFAULT_VALUE", + "char_set": 224, + "max_size": 128 + } + }, + { + "ordinal": 7, + "name": "maybe_redeemer_user_token: tokens::tokens::users::UserToken", + "type_info": { + "type": "VarString", + "flags": "MULTIPLE_KEY | BINARY", + "char_set": 224, + "max_size": 128 + } + }, + { + "ordinal": 8, + "name": "maybe_redeemer_username", + "type_info": { + "type": "VarString", + "flags": "UNIQUE_KEY | BINARY | NO_DEFAULT_VALUE", + "char_set": 224, + "max_size": 80 + } + }, + { + "ordinal": 9, + "name": "maybe_redeemer_display_name", + "type_info": { + "type": "VarString", + "flags": "BINARY | NO_DEFAULT_VALUE", + "char_set": 224, + "max_size": 80 + } + }, + { + "ordinal": 10, + "name": "maybe_redeemer_gravatar_hash", + "type_info": { + "type": "String", + "flags": "BINARY | NO_DEFAULT_VALUE", + "char_set": 224, + "max_size": 128 + } + }, + { + "ordinal": 11, + "name": "created_at", + "type_info": { + "type": "Timestamp", + "flags": "NOT_NULL | MULTIPLE_KEY | BINARY | TIMESTAMP", + "char_set": 63, + "max_size": 19 + } + }, + { + "ordinal": 12, + "name": "maybe_redeemed_at", + "type_info": { + "type": "Timestamp", + "flags": "MULTIPLE_KEY | BINARY", + "char_set": 63, + "max_size": 19 + } + } + ], + "parameters": { + "Right": 1 + }, + "nullable": [ + false, + false, + false, + true, + true, + true, + true, + true, + true, + true, + true, + false, + true + ] + }, + "hash": "74b401958826a7e640540f248114f57a941324787d023783f84251e8dd56eeb7" +} diff --git a/crates/lib/mysql_queries/src/queries/users/user/set_user_feature_flags.rs b/crates/lib/mysql_queries/src/queries/users/user/set_user_feature_flags.rs index 20fb6f9e9a..4e8c86425b 100644 --- a/crates/lib/mysql_queries/src/queries/users/user/set_user_feature_flags.rs +++ b/crates/lib/mysql_queries/src/queries/users/user/set_user_feature_flags.rs @@ -1,16 +1,25 @@ use anyhow::anyhow; use sqlx::MySqlPool; +use composite_identifiers::by_table::audit_logs::audit_log_entity::AuditLogEntity; +use enums::by_table::audit_logs::audit_log_entity_action::AuditLogEntityAction; use errors::AnyhowResult; use tokens::tokens::users::UserToken; +use crate::queries::audit_logs::insert_audit_log::{insert_audit_log, InsertAuditLogArgs}; pub struct SetUserFeatureFlagArgs<'a> { + // The action's target user token. pub subject_user_token: &'a UserToken, // Comma separated string of feature flags. pub maybe_feature_flags: Option<&'a str>, - pub mod_user_token: &'a UserToken, + // Actor IP address. + pub ip_address: &'a str, + + // If a moderator is changing the flags, the moderator user token + pub maybe_mod_user_token: Option<&'a UserToken>, + pub mysql_pool: &'a MySqlPool, } @@ -20,21 +29,32 @@ pub async fn set_user_feature_flags(args: SetUserFeatureFlagArgs<'_>) -> AnyhowR UPDATE users SET maybe_feature_flags = ?, - maybe_mod_user_token = ?, version = version + 1 WHERE users.token = ? LIMIT 1 "#, args.maybe_feature_flags, - args.mod_user_token, args.subject_user_token, ) .execute(args.mysql_pool) .await; - match query_result { - Ok(_) => Ok(()), - Err(err) => Err(anyhow!("error with query: {:?}", err)), + if let Err(err) = query_result { + return Err(anyhow!("error with query: {:?}", err)); } + + // NB: fail open + let _r = insert_audit_log(InsertAuditLogArgs { + entity: &AuditLogEntity::User(args.subject_user_token.clone()), + entity_action: AuditLogEntityAction::EditFeatures, + maybe_actor_user_token: args.maybe_mod_user_token, + maybe_actor_anonymous_visitor_token: None, + actor_ip_address: &args.ip_address, + is_actor_moderator: true, + mysql_executor: args.mysql_pool, + phantom: Default::default(), + }).await; + + Ok(()) } diff --git a/crates/service/web/storyteller_web/src/http_server/endpoints/beta_keys/redeem_beta_key_handler.rs b/crates/service/web/storyteller_web/src/http_server/endpoints/beta_keys/redeem_beta_key_handler.rs index 6af7c3869f..a8858e0b51 100644 --- a/crates/service/web/storyteller_web/src/http_server/endpoints/beta_keys/redeem_beta_key_handler.rs +++ b/crates/service/web/storyteller_web/src/http_server/endpoints/beta_keys/redeem_beta_key_handler.rs @@ -8,10 +8,12 @@ use log::warn; use utoipa::ToSchema; use enums::by_table::comments::comment_entity_type::CommentEntityType; +use enums::by_table::users::user_feature_flag::UserFeatureFlag; use http_server_common::request::get_request_ip::get_request_ip; use mysql_queries::queries::beta_keys::get_beta_key_by_value::get_beta_key_by_value; use mysql_queries::queries::comments::comment_entity_token::CommentEntityToken; use mysql_queries::queries::comments::insert_comment::{insert_comment, InsertCommentArgs}; +use mysql_queries::queries::users::user::set_user_feature_flags::{set_user_feature_flags, SetUserFeatureFlagArgs}; use tokens::tokens::comments::CommentToken; use tokens::tokens::media_files::MediaFileToken; use tokens::tokens::model_weights::ModelWeightToken; @@ -22,6 +24,7 @@ use tokens::tokens::w2l_results::W2lResultToken; use tokens::tokens::w2l_templates::W2lTemplateToken; use user_input_common::check_for_slurs::contains_slurs; use user_input_common::markdown_to_html::markdown_to_html; +use users_component::session::lookup::user_session_feature_flags::UserSessionFeatureFlags; use crate::http_server::endpoints::beta_keys::list_beta_keys_handler::ListBetaKeysError; use crate::http_server::endpoints::moderation::user_feature_flags::edit_user_feature_flags_handler::EditUserFeatureFlagsError; @@ -122,6 +125,28 @@ pub async fn redeem_beta_key_handler( return Err(RedeemBetaKeyError::BadInput("beta key already redeemed".to_string())); } + let mut user_feature_flags = + UserSessionFeatureFlags::new(user_session.maybe_feature_flags.as_deref()); + + user_feature_flags.add_flags([ + UserFeatureFlag::Studio, + UserFeatureFlag::VideoStyleTransfer, + ]); + + let ip_address = get_request_ip(&http_request); + + set_user_feature_flags(SetUserFeatureFlagArgs { + subject_user_token: &user_session.user_token, + maybe_feature_flags: user_feature_flags.maybe_serialize_string().as_deref(), + maybe_mod_user_token: None, + ip_address: &ip_address, + mysql_pool: &server_state.mysql_pool, + }).await + .map_err(|e| { + warn!("Could not set flags: {:?}", e); + RedeemBetaKeyError::ServerError + })?; + let response = RedeemBetaKeySuccessResponse { success: true, }; diff --git a/crates/service/web/storyteller_web/src/http_server/endpoints/moderation/user_feature_flags/edit_user_feature_flags_handler.rs b/crates/service/web/storyteller_web/src/http_server/endpoints/moderation/user_feature_flags/edit_user_feature_flags_handler.rs index 211f8a4ee7..2f0bbd8091 100644 --- a/crates/service/web/storyteller_web/src/http_server/endpoints/moderation/user_feature_flags/edit_user_feature_flags_handler.rs +++ b/crates/service/web/storyteller_web/src/http_server/endpoints/moderation/user_feature_flags/edit_user_feature_flags_handler.rs @@ -198,7 +198,8 @@ pub async fn edit_user_feature_flags_handler( set_user_feature_flags(SetUserFeatureFlagArgs { subject_user_token: &user_profile.user_token, maybe_feature_flags: user_feature_flags.maybe_serialize_string().as_deref(), - mod_user_token: &user_session.user_token, + maybe_mod_user_token: Some(&user_session.user_token), + ip_address: &ip_address, mysql_pool: &server_state.mysql_pool, }).await .map_err(|e| { @@ -206,18 +207,6 @@ pub async fn edit_user_feature_flags_handler( EditUserFeatureFlagsError::ServerError })?; - // NB: fail open - let _r = insert_audit_log(InsertAuditLogArgs { - entity: &AuditLogEntity::User(user_profile.user_token), - entity_action: AuditLogEntityAction::EditFeatures, - maybe_actor_user_token: Some(&user_session.user_token), - maybe_actor_anonymous_visitor_token: None, - actor_ip_address: &ip_address, - is_actor_moderator: true, - mysql_executor: &server_state.mysql_pool, - phantom: Default::default(), - }).await; - //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);