fix(admin): save premium limits and model changes (#3386)

This commit is contained in:
Hampus authored and GitHub committed 2026-10-11 13:26:39 +02:00
1 parent 7d2c51a57f
commit 6acd7f3d22
18 files changed
+1035 -59

No files matched your search

+1 -1
View File
@@ -143,7 +143,7 @@ pub(super) fn billing_result<T>(result: Result<T, ApiError>) -> FlashData {
}
}
fn validation_message(error: &ApiError) -> Option<String> {
pub(super) fn validation_message(error: &ApiError) -> Option<String> {
let ApiError::Http {
status: 400,
message,
+176 -8
View File
@@ -18,7 +18,7 @@ use crate::{
InstanceIntegrationsUpdateRequest, InstanceMediaUpdateRequest,
InstancePolicyUpdateRequest, InstanceRegistrationConfigUpdateRequest,
InstanceServicesUpdateRequest, InstanceYoutubeIntegrationUpdateRequest,
LimitConfigUpdateRequest, LimitRule, LimitRuleFilters, PremiumMode,
LimitConfigUpdateRequest, LimitKeyMetadata, LimitRule, LimitRuleFilters, PremiumMode,
PushRelayConfigUpdateRequest, RegistrationMode, SsoConfigUpdateRequest,
VOICE_P2P_MAX_PARTICIPANTS_RANGE, VoiceE2eeScope, VoiceP2pConfigUpdateRequest,
},
@@ -405,12 +405,17 @@ fn render_fragment_with_toast(markup: Markup, flash: &FlashData) -> Response {
response
}
fn instance_config_result<T, E: std::fmt::Display>(result: Result<T, E>) -> FlashData {
fn instance_config_result<T>(result: Result<T, crate::api::client::ApiError>) -> FlashData {
match result {
Ok(_) => FlashData::success("Instance config updated"),
Err(error) => {
tracing::warn!(%error, "admin API request failed: update instance config");
FlashData::error("Failed to update instance config")
match super::billing_actions::validation_message(&error) {
Some(message) => {
FlashData::error(format!("Failed to update instance config: {message}"))
}
None => FlashData::error("Failed to update instance config"),
}
}
}
}
@@ -1042,6 +1047,7 @@ fn update_limit_rule_values(
rule: &mut LimitRule,
form: &MultiValueForm,
limit_keys: &[String],
metadata: &std::collections::BTreeMap<String, LimitKeyMetadata>,
fallback_limits: Option<&std::collections::BTreeMap<String, u64>>,
) {
let mut limits = std::collections::BTreeMap::new();
@@ -1055,12 +1061,19 @@ fn update_limit_rule_values(
{
limits.extend(defaults.clone());
}
for key in limit_keys {
if metadata.get(key).is_some_and(|metadata| metadata.is_toggle)
&& rule.limits.contains_key(key)
{
limits.entry(key.clone()).or_insert(0);
}
}
rule.limits = limits;
rule.filters = build_limit_filters(form);
}
fn limit_config_result<T, E: std::fmt::Display>(
result: Result<T, E>,
fn limit_config_result<T>(
result: Result<T, crate::api::client::ApiError>,
success_message: &'static str,
error_message: &'static str,
) -> FlashData {
@@ -1068,7 +1081,10 @@ fn limit_config_result<T, E: std::fmt::Display>(
Ok(_) => FlashData::success(success_message),
Err(error) => {
tracing::warn!(%error, "admin API request failed: update limit config");
FlashData::error(error_message)
match super::billing_actions::validation_message(&error) {
Some(message) => FlashData::error(format!("{error_message}: {message}")),
None => FlashData::error(error_message),
}
}
}
}
@@ -1137,7 +1153,13 @@ pub async fn limit_config_post(
.defaults
.get(&rule_id)
.or_else(|| current.defaults.get("default"));
update_limit_rule_values(rule, &form, &current.limit_keys, fallback);
update_limit_rule_values(
rule,
&form,
&current.limit_keys,
&current.metadata,
fallback,
);
let request = LimitConfigUpdateRequest { limit_config };
let result = client.update_limit_config(&request).await;
let flash = limit_config_result(
@@ -1147,6 +1169,43 @@ pub async fn limit_config_post(
);
return redirect_back_with_flash(base, "/limit-config", flash, secure_cookies);
}
"reset" => {
let rule = aq
.rule
.as_deref()
.and_then(clean_string)
.and_then(|rule_id| {
limit_config
.rules
.iter_mut()
.find(|rule| rule.id == rule_id)
});
let Some(rule) = rule else {
return redirect_back_with_flash(
base,
"/limit-config",
FlashData::error("Rule not found"),
secure_cookies,
);
};
let Some(defaults) = current.defaults.get(&rule.id) else {
return redirect_back_with_flash(
base,
"/limit-config",
FlashData::error("This rule has no defaults to reset to"),
secure_cookies,
);
};
rule.limits = defaults.clone();
let request = LimitConfigUpdateRequest { limit_config };
let result = client.update_limit_config(&request).await;
let flash = limit_config_result(
result,
"Limit rule reset to defaults",
"Failed to reset limit rule",
);
return redirect_back_with_flash(base, "/limit-config", flash, secure_cookies);
}
"delete" => {
let rule_id = match aq.rule.as_deref().and_then(clean_string) {
Some(rule_id) => rule_id,
@@ -1360,6 +1419,109 @@ mod tests {
);
}
#[test]
fn instance_config_result_surfaces_the_api_validation_message() {
let error = crate::api::client::ApiError::Http {
status: 400,
message: serde_json::json!({
"code": "INVALID_FORM_BODY",
"message": "Input Validation Error",
"errors": [{
"path": "policy.premium_mode",
"code": "X",
"message": "Disable billing before switching the premium mode to everyone"
}]
})
.to_string(),
};
assert_eq!(
instance_config_result::<()>(Err(error)).message,
"Failed to update instance config: policy.premium_mode: Disable billing before switching the premium mode to everyone"
);
}
#[test]
fn limit_config_result_surfaces_the_api_validation_message() {
let error = crate::api::client::ApiError::Http {
status: 400,
message: serde_json::json!({
"code": "INVALID_FORM_BODY",
"message": "Input Validation Error",
"errors": [{
"path": "limit_config.rules.0.limits.max_bio_length",
"code": "VALUE_MUST_BE_INTEGER_IN_RANGE",
"message": "Value must be an integer between 0 and 320."
}]
})
.to_string(),
};
assert_eq!(
limit_config_result::<()>(Err(error), "Saved", "Failed to update limit configuration")
.message,
"Failed to update limit configuration: limit_config.rules.0.limits.max_bio_length: Value must be an integer between 0 and 320."
);
}
#[test]
fn update_limit_rule_values_saves_unchecked_toggles_as_off() {
let toggle = |key: &str, is_toggle: bool| {
(
key.to_owned(),
LimitKeyMetadata {
key: key.to_owned(),
label: key.to_owned(),
description: String::new(),
category: "features".to_owned(),
scope: "user".to_owned(),
is_toggle,
unit: None,
min: None,
max: None,
},
)
};
let metadata = std::collections::BTreeMap::from([
toggle("feature_animated_avatar", true),
toggle("feature_animated_banner", true),
toggle("max_guilds", false),
]);
let limit_keys = metadata.keys().cloned().collect::<Vec<_>>();
let mut rule = LimitRule {
id: "premium".to_owned(),
filters: None,
limits: std::collections::BTreeMap::from([
("feature_animated_avatar".to_owned(), 1),
("feature_animated_banner".to_owned(), 1),
("max_guilds".to_owned(), 200),
]),
modified_fields: None,
};
let form =
MultiValueForm::parse(b"traits=premium&feature_animated_banner=1&max_guilds=250");
update_limit_rule_values(&mut rule, &form, &limit_keys, &metadata, None);
assert_eq!(
rule.limits,
std::collections::BTreeMap::from([
("feature_animated_avatar".to_owned(), 0),
("feature_animated_banner".to_owned(), 1),
("max_guilds".to_owned(), 250),
])
);
let mut custom = LimitRule {
id: "vip".to_owned(),
filters: None,
limits: std::collections::BTreeMap::from([("max_guilds".to_owned(), 300)]),
modified_fields: None,
};
let form = MultiValueForm::parse(b"traits=vip&max_guilds=300");
update_limit_rule_values(&mut custom, &form, &limit_keys, &metadata, None);
assert_eq!(
custom.limits,
std::collections::BTreeMap::from([("max_guilds".to_owned(), 300)])
);
}
#[test]
fn parse_experiment_user_ids_splits_newlines_and_commas() {
assert_eq!(
@@ -1823,7 +1985,13 @@ mod tests {
limits: std::collections::BTreeMap::new(),
modified_fields: None,
};
update_limit_rule_values(&mut rule, &form, &["message_send".to_owned()], None);
update_limit_rule_values(
&mut rule,
&form,
&["message_send".to_owned()],
&std::collections::BTreeMap::new(),
None,
);
assert_eq!(rule.limits.get("message_send"), Some(&1));
assert_eq!(
rule.filters.expect("filters").traits,