edits; list user feature flags

This commit is contained in:
Brandon Thomas committed 2026-04-24 03:42:25 -04:00
1 parent ae68604082
commit 0ddcd178c8
4 files changed
+75 -35

No files matched your search

@@ -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);
}
}
@@ -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<EditUserFeatureFlagPathInfo>,
request: Json<EditUserFeatureFlagsRequest>,
server_state: Data<Arc<ServerState>>,
//redis_ttl_cache: Data<RedisTtlCache>,
redis_pool: Data<r2d2::Pool<Client>>,
) -> Result<HttpResponse, EditUserFeatureFlagsError> {
@@ -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<Option<String>, _> = redis.del(&cache_key);
let _r: Result<Option<String>, _> = redis.del(&cache_key);
}
Ok(simple_json_success())
@@ -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<FeatureFlagDescriptor> = 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();
@@ -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<String>,
}
@@ -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,
}))
}