mirror of
https://github.com/fluxerapp/fluxer
synced 2026-10-17 21:15:32 +09:00
refactor(admin): tidy the admin routes and templates (#2734)
This commit is contained in:
1 parent
5afbf67a70
commit
33737e0f79
48 files changed
+697
-679
No files matched your search
@@ -81,7 +81,7 @@ async fn admin_api_keys_post(
|
||||
"create" => {
|
||||
let name = form.clean("name").unwrap_or_default();
|
||||
let acls = form.list_values_any(&["acls[]", "acls"]);
|
||||
return match client.create_api_key(&name, &acls).await {
|
||||
match client.create_api_key(&name, &acls).await {
|
||||
Ok(created) => {
|
||||
let keys = client
|
||||
.list_api_keys()
|
||||
@@ -107,29 +107,38 @@ async fn admin_api_keys_post(
|
||||
is_htmx,
|
||||
)
|
||||
}
|
||||
};
|
||||
}
|
||||
"revoke" => {
|
||||
if let Some(key_id) = form.clean("key_id") {
|
||||
let result = client.revoke_api_key(&key_id).await;
|
||||
let flash = match result.log_error("revoke API key") {
|
||||
Some(_) => FlashData::success("API key revoked."),
|
||||
None => FlashData::error("Failed to revoke API key"),
|
||||
};
|
||||
return flash::redirect_with_flash(
|
||||
&format!("{base}/admin-api-keys"),
|
||||
flash,
|
||||
config.secure_cookies(),
|
||||
);
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
"revoke" => {
|
||||
let Some(key_id) = form.clean("key_id") else {
|
||||
return admin_api_key_flash_response(
|
||||
config,
|
||||
FlashData::error("API key ID is required"),
|
||||
is_htmx,
|
||||
);
|
||||
};
|
||||
let result = client.revoke_api_key(&key_id).await;
|
||||
let flash = match result.log_error("revoke API key") {
|
||||
Some(_) => FlashData::success("API key revoked."),
|
||||
None => FlashData::error("Failed to revoke API key"),
|
||||
};
|
||||
flash::redirect_with_flash(
|
||||
&format!("{base}/admin-api-keys"),
|
||||
flash,
|
||||
config.secure_cookies(),
|
||||
)
|
||||
}
|
||||
"" => admin_api_key_flash_response(
|
||||
config,
|
||||
FlashData::error("API key action is required"),
|
||||
is_htmx,
|
||||
),
|
||||
_ => admin_api_key_flash_response(
|
||||
config,
|
||||
FlashData::error("Unknown API key action"),
|
||||
is_htmx,
|
||||
),
|
||||
}
|
||||
flash::redirect_with_flash(
|
||||
&format!("{base}/admin-api-keys"),
|
||||
FlashData::success(format!("API key action '{action}' completed.")),
|
||||
config.secure_cookies(),
|
||||
)
|
||||
}
|
||||
|
||||
fn admin_api_key_flash_response(
|
||||
|
||||
@@ -16,7 +16,7 @@ use axum::{
|
||||
|
||||
use super::ActionQuery;
|
||||
use super::bans_actions::{
|
||||
BanFormData, custom_flash, execute_ban, extract_value, flash_response, htmx_flash,
|
||||
BanFormData, custom_flash, execute_ban, extract_value, flash_response, render_inline_flash,
|
||||
};
|
||||
|
||||
pub fn router() -> Router<AppState> {
|
||||
@@ -105,7 +105,7 @@ async fn generic_ban_post(
|
||||
form.audit_log_reason.as_deref(),
|
||||
)
|
||||
.await;
|
||||
flash_response(config, auth, is_htmx, &level, &msg, ban_cfg, csrf_token)
|
||||
flash_response(config, auth, is_htmx, level, &msg, ban_cfg, csrf_token)
|
||||
}
|
||||
|
||||
macro_rules! ban_post {
|
||||
@@ -171,19 +171,22 @@ async fn url_domain_bans_post(
|
||||
Query::try_from_uri(request.uri()).unwrap_or(Query(ActionQuery { action: None }));
|
||||
let form: BanFormData = match Form::from_request(request, &state).await {
|
||||
Ok(Form(f)) => f,
|
||||
Err(_) => return htmx_flash("error", "Invalid form data", &headers),
|
||||
Err(_) => return render_inline_flash("error", "Invalid form data"),
|
||||
};
|
||||
let is_htmx = htmx::is_htmx_request(&headers);
|
||||
let client = AdminApiClient::new(state.http_client(), config, &auth.0.session);
|
||||
let domain = form.domain.as_deref().unwrap_or("").trim().to_owned();
|
||||
if domain.is_empty() {
|
||||
return htmx_flash("error", "Domain is required", &headers);
|
||||
return render_inline_flash("error", "Domain is required");
|
||||
}
|
||||
let action = aq.action.as_deref().unwrap_or("");
|
||||
let (level, msg) = match action {
|
||||
"ban" => {
|
||||
let m_sub = form.match_subdomains.as_deref() == Some("true");
|
||||
match client.ban_url_domain(&domain, m_sub).await {
|
||||
match client
|
||||
.ban_url_domain(&domain, m_sub, form.audit_log_reason.as_deref())
|
||||
.await
|
||||
{
|
||||
Ok(()) => ("success", format!("Domain {domain} banned successfully")),
|
||||
Err(error) => {
|
||||
tracing::warn!(%error, domain, "admin API request failed: ban URL domain");
|
||||
@@ -191,7 +194,10 @@ async fn url_domain_bans_post(
|
||||
}
|
||||
}
|
||||
}
|
||||
"unban" => match client.unban_url_domain(&domain).await {
|
||||
"unban" => match client
|
||||
.unban_url_domain(&domain, form.audit_log_reason.as_deref())
|
||||
.await
|
||||
{
|
||||
Ok(()) => ("success", format!("Domain {domain} unbanned")),
|
||||
Err(error) => {
|
||||
tracing::warn!(%error, domain, "admin API request failed: unban URL domain");
|
||||
@@ -247,25 +253,31 @@ async fn profile_substring_bans_post(
|
||||
Query::try_from_uri(request.uri()).unwrap_or(Query(ActionQuery { action: None }));
|
||||
let form: BanFormData = match Form::from_request(request, &state).await {
|
||||
Ok(Form(f)) => f,
|
||||
Err(_) => return htmx_flash("error", "Invalid form data", &headers),
|
||||
Err(_) => return render_inline_flash("error", "Invalid form data"),
|
||||
};
|
||||
let is_htmx = htmx::is_htmx_request(&headers);
|
||||
let client = AdminApiClient::new(state.http_client(), config, &auth.0.session);
|
||||
let scope = form.scope.as_deref().unwrap_or("").trim().to_owned();
|
||||
let substring = form.substring.as_deref().unwrap_or("").trim().to_owned();
|
||||
if scope.is_empty() || substring.is_empty() {
|
||||
return htmx_flash("error", "Scope and substring required", &headers);
|
||||
return render_inline_flash("error", "Scope and substring required");
|
||||
}
|
||||
let action = aq.action.as_deref().unwrap_or("");
|
||||
let (level, msg) = match action {
|
||||
"ban" => match client.ban_profile_substring(&scope, &substring).await {
|
||||
"ban" => match client
|
||||
.ban_profile_substring(&scope, &substring, form.audit_log_reason.as_deref())
|
||||
.await
|
||||
{
|
||||
Ok(()) => ("success", format!("\"{substring}\" banned for {scope}")),
|
||||
Err(error) => {
|
||||
tracing::warn!(%error, scope, substring, "admin API request failed: ban profile substring");
|
||||
("error", format!("Failed to ban substring for {scope}"))
|
||||
}
|
||||
},
|
||||
"unban" => match client.unban_profile_substring(&scope, &substring).await {
|
||||
"unban" => match client
|
||||
.unban_profile_substring(&scope, &substring, form.audit_log_reason.as_deref())
|
||||
.await
|
||||
{
|
||||
Ok(()) => ("success", format!("\"{substring}\" unbanned for {scope}")),
|
||||
Err(error) => {
|
||||
tracing::warn!(%error, scope, substring, "admin API request failed: unban profile substring");
|
||||
|
||||
@@ -1,17 +1,13 @@
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
|
||||
use crate::api::client::AdminApiClient;
|
||||
use crate::api::client::{AdminApiClient, ApiResult};
|
||||
use crate::api::types::{FlashLevel, FlashMessage};
|
||||
use crate::middleware::auth::AuthContext;
|
||||
use crate::templates;
|
||||
use axum::{
|
||||
http::HeaderMap,
|
||||
response::{Html, IntoResponse, Response},
|
||||
};
|
||||
use axum::response::{Html, IntoResponse, Response};
|
||||
use serde::Deserialize;
|
||||
|
||||
#[derive(Deserialize)]
|
||||
#[allow(dead_code)]
|
||||
pub struct BanFormData {
|
||||
#[serde(default)]
|
||||
pub ip: Option<String>,
|
||||
@@ -65,7 +61,7 @@ pub async fn execute_ban(
|
||||
bulk_hashes: Option<&str>,
|
||||
bulk_sha256_list: Option<&str>,
|
||||
audit_log_reason: Option<&str>,
|
||||
) -> (String, String) {
|
||||
) -> (&'static str, String) {
|
||||
if (action == "bulk-ban" || action == "bulk-ban-files") && ban_type == "file-sha-bans" {
|
||||
let raw_hashes = if action == "bulk-ban-files" {
|
||||
bulk_sha256_list
|
||||
@@ -75,13 +71,13 @@ pub async fn execute_ban(
|
||||
return execute_bulk_ban(client, raw_hashes, audit_log_reason).await;
|
||||
}
|
||||
if value.is_empty() {
|
||||
return ("error".into(), "Value is required".into());
|
||||
return ("error", "Value is required".into());
|
||||
}
|
||||
match action {
|
||||
"ban" => execute_single_ban(client, ban_type, value, audit_log_reason).await,
|
||||
"unban" => execute_single_unban(client, ban_type, value, audit_log_reason).await,
|
||||
"check" => execute_check(client, ban_type, value).await,
|
||||
_ => ("error".into(), "Unknown action".into()),
|
||||
_ => ("error", "Unknown action".into()),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -89,7 +85,7 @@ async fn execute_bulk_ban(
|
||||
client: &AdminApiClient,
|
||||
bulk_hashes: Option<&str>,
|
||||
audit_log_reason: Option<&str>,
|
||||
) -> (String, String) {
|
||||
) -> (&'static str, String) {
|
||||
let hashes: Vec<String> = bulk_hashes
|
||||
.unwrap_or("")
|
||||
.split(|c: char| c.is_whitespace() || c == ',' || c == ';')
|
||||
@@ -98,18 +94,15 @@ async fn execute_bulk_ban(
|
||||
.collect();
|
||||
if hashes.is_empty() {
|
||||
return (
|
||||
"error".into(),
|
||||
"error",
|
||||
"No valid 64-character hex hashes found in input".into(),
|
||||
);
|
||||
}
|
||||
match client.bulk_ban_file_shas(&hashes, audit_log_reason).await {
|
||||
Ok(r) => (
|
||||
"success".into(),
|
||||
format!("Bulk ban job created: {}", r.job_id),
|
||||
),
|
||||
Ok(r) => ("success", format!("Bulk ban job created: {}", r.job_id)),
|
||||
Err(error) => {
|
||||
tracing::warn!(%error, "admin API request failed: bulk ban file SHAs");
|
||||
("error".into(), "Failed to enqueue bulk ban job".into())
|
||||
("error", "Failed to enqueue bulk ban job".into())
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -119,29 +112,26 @@ async fn execute_single_ban(
|
||||
ban_type: &str,
|
||||
value: &str,
|
||||
audit_log_reason: Option<&str>,
|
||||
) -> (String, String) {
|
||||
let success = format!("{value} banned successfully");
|
||||
let failure = format!("Failed to ban {value}");
|
||||
match ban_type {
|
||||
"ip-bans" => ban_action_result(client.ban_ip(value).await, success, failure),
|
||||
"email-bans" => ban_action_result(client.ban_email(value).await, success, failure),
|
||||
"suspicious-email-domains" => ban_action_result(
|
||||
client.add_suspicious_email_domain(value).await,
|
||||
success,
|
||||
failure,
|
||||
),
|
||||
"phrase-bans" => ban_action_result(client.ban_phrase(value).await, success, failure),
|
||||
"url-bans" => ban_action_result(client.ban_url(value).await, success, failure),
|
||||
"file-sha-bans" => ban_action_result(
|
||||
client.ban_file_sha(value, audit_log_reason).await,
|
||||
success,
|
||||
failure,
|
||||
),
|
||||
"avatar-hash-bans" => {
|
||||
ban_action_result(client.ban_avatar_hash(value).await, success, failure)
|
||||
) -> (&'static str, String) {
|
||||
let result = match ban_type {
|
||||
"ip-bans" => client.ban_ip(value, audit_log_reason).await,
|
||||
"email-bans" => client.ban_email(value, audit_log_reason).await,
|
||||
"suspicious-email-domains" => {
|
||||
client
|
||||
.add_suspicious_email_domain(value, audit_log_reason)
|
||||
.await
|
||||
}
|
||||
_ => ("error".into(), "Unknown ban type".into()),
|
||||
}
|
||||
"phrase-bans" => client.ban_phrase(value, audit_log_reason).await,
|
||||
"url-bans" => client.ban_url(value, audit_log_reason).await,
|
||||
"file-sha-bans" => client.ban_file_sha(value, audit_log_reason).await,
|
||||
"avatar-hash-bans" => client.ban_avatar_hash(value, audit_log_reason).await,
|
||||
_ => return ("error", "Unknown ban type".into()),
|
||||
};
|
||||
ban_action_result(
|
||||
result,
|
||||
format!("{value} banned successfully"),
|
||||
format!("Failed to ban {value}"),
|
||||
)
|
||||
}
|
||||
|
||||
async fn execute_single_unban(
|
||||
@@ -149,32 +139,33 @@ async fn execute_single_unban(
|
||||
ban_type: &str,
|
||||
value: &str,
|
||||
audit_log_reason: Option<&str>,
|
||||
) -> (String, String) {
|
||||
let success = format!("{value} unbanned successfully");
|
||||
let failure = format!("Failed to unban {value}");
|
||||
match ban_type {
|
||||
"ip-bans" => ban_action_result(client.unban_ip(value).await, success, failure),
|
||||
"email-bans" => ban_action_result(client.unban_email(value).await, success, failure),
|
||||
"suspicious-email-domains" => ban_action_result(
|
||||
client.remove_suspicious_email_domain(value).await,
|
||||
success,
|
||||
failure,
|
||||
),
|
||||
"phrase-bans" => ban_action_result(client.unban_phrase(value).await, success, failure),
|
||||
"url-bans" => ban_action_result(client.unban_url(value).await, success, failure),
|
||||
"file-sha-bans" => ban_action_result(
|
||||
client.unban_file_sha(value, audit_log_reason).await,
|
||||
success,
|
||||
failure,
|
||||
),
|
||||
"avatar-hash-bans" => {
|
||||
ban_action_result(client.unban_avatar_hash(value).await, success, failure)
|
||||
) -> (&'static str, String) {
|
||||
let result = match ban_type {
|
||||
"ip-bans" => client.unban_ip(value, audit_log_reason).await,
|
||||
"email-bans" => client.unban_email(value, audit_log_reason).await,
|
||||
"suspicious-email-domains" => {
|
||||
client
|
||||
.remove_suspicious_email_domain(value, audit_log_reason)
|
||||
.await
|
||||
}
|
||||
_ => ("error".into(), "Unknown ban type".into()),
|
||||
}
|
||||
"phrase-bans" => client.unban_phrase(value, audit_log_reason).await,
|
||||
"url-bans" => client.unban_url(value, audit_log_reason).await,
|
||||
"file-sha-bans" => client.unban_file_sha(value, audit_log_reason).await,
|
||||
"avatar-hash-bans" => client.unban_avatar_hash(value, audit_log_reason).await,
|
||||
_ => return ("error", "Unknown ban type".into()),
|
||||
};
|
||||
ban_action_result(
|
||||
result,
|
||||
format!("{value} unbanned successfully"),
|
||||
format!("Failed to unban {value}"),
|
||||
)
|
||||
}
|
||||
|
||||
async fn execute_check(client: &AdminApiClient, ban_type: &str, value: &str) -> (String, String) {
|
||||
async fn execute_check(
|
||||
client: &AdminApiClient,
|
||||
ban_type: &str,
|
||||
value: &str,
|
||||
) -> (&'static str, String) {
|
||||
let result = match ban_type {
|
||||
"ip-bans" => client.check_ip_ban(value).await,
|
||||
"email-bans" => client.check_email_ban(value).await,
|
||||
@@ -183,28 +174,28 @@ async fn execute_check(client: &AdminApiClient, ban_type: &str, value: &str) ->
|
||||
"url-bans" => client.check_url_ban(value).await,
|
||||
"file-sha-bans" => client.check_file_sha_ban(value).await,
|
||||
"avatar-hash-bans" => client.check_avatar_hash_ban(value).await,
|
||||
_ => return ("error".into(), "Unknown ban type".into()),
|
||||
_ => return ("error", "Unknown ban type".into()),
|
||||
};
|
||||
match result {
|
||||
Ok(r) if r.banned => ("info".into(), format!("{value} is banned")),
|
||||
Ok(_) => ("info".into(), format!("{value} is NOT banned")),
|
||||
Ok(r) if r.banned => ("info", format!("{value} is banned")),
|
||||
Ok(_) => ("info", format!("{value} is NOT banned")),
|
||||
Err(error) => {
|
||||
tracing::warn!(%error, ban_type, value, "admin API request failed: check ban status");
|
||||
("error".into(), "Error checking ban status".into())
|
||||
("error", "Error checking ban status".into())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn ban_action_result<T, E: std::fmt::Display>(
|
||||
result: Result<T, E>,
|
||||
fn ban_action_result(
|
||||
result: ApiResult<()>,
|
||||
success_message: String,
|
||||
error_message: String,
|
||||
) -> (String, String) {
|
||||
) -> (&'static str, String) {
|
||||
match result {
|
||||
Ok(_) => ("success".into(), success_message),
|
||||
Ok(()) => ("success", success_message),
|
||||
Err(error) => {
|
||||
tracing::warn!(%error, "admin API request failed: ban action");
|
||||
("error".into(), error_message)
|
||||
("error", error_message)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -228,11 +219,6 @@ pub fn flash_response(
|
||||
}
|
||||
}
|
||||
|
||||
pub fn htmx_flash(level: &str, message: &str, headers: &HeaderMap) -> Response {
|
||||
let _ = headers;
|
||||
render_inline_flash(level, message)
|
||||
}
|
||||
|
||||
pub fn render_inline_flash(level: &str, message: &str) -> Response {
|
||||
let (border, bg, text) = match level {
|
||||
"success" => ("border-green-300", "bg-green-50", "text-green-800"),
|
||||
|
||||
@@ -1,14 +1,14 @@
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
|
||||
use crate::{
|
||||
api::client::AdminApiClient,
|
||||
api::{client::AdminApiClient, generated::types::GiftCodeDurationTypeSchema},
|
||||
middleware::{
|
||||
auth::AuthContext,
|
||||
csrf::CsrfToken,
|
||||
flash::{self, FlashData},
|
||||
},
|
||||
state::AppState,
|
||||
templates,
|
||||
templates::{self, pages::gift_codes::MAX_GIFT_CODES},
|
||||
};
|
||||
use axum::{
|
||||
Form, Router,
|
||||
@@ -28,11 +28,11 @@ struct GiftCodesForm {
|
||||
#[serde(default)]
|
||||
_csrf: Option<String>,
|
||||
#[serde(default)]
|
||||
count: Option<String>,
|
||||
count: Option<u32>,
|
||||
#[serde(default)]
|
||||
duration_type: Option<String>,
|
||||
duration_type: Option<GiftCodeDurationTypeSchema>,
|
||||
#[serde(default)]
|
||||
duration_quantity: Option<String>,
|
||||
duration_quantity: Option<u32>,
|
||||
}
|
||||
|
||||
pub fn router() -> Router<AppState> {
|
||||
@@ -86,21 +86,17 @@ async fn gift_codes_post(
|
||||
);
|
||||
}
|
||||
};
|
||||
let count = form
|
||||
.count
|
||||
.as_deref()
|
||||
.and_then(|s| s.parse::<u32>().ok())
|
||||
.unwrap_or(1)
|
||||
.clamp(1, 100);
|
||||
let dur_type = form.duration_type.as_deref().unwrap_or("month");
|
||||
let dur_qty = form
|
||||
.duration_quantity
|
||||
.as_deref()
|
||||
.and_then(|s| s.parse::<u32>().ok())
|
||||
.unwrap_or(1);
|
||||
let count = form.count.unwrap_or(1).clamp(1, MAX_GIFT_CODES);
|
||||
let duration_type = form
|
||||
.duration_type
|
||||
.unwrap_or(GiftCodeDurationTypeSchema::Months);
|
||||
let duration_quantity = form.duration_quantity.unwrap_or(1);
|
||||
let client = AdminApiClient::new(state.http_client(), config, &auth.0.session);
|
||||
let secure_cookies = config.secure_cookies();
|
||||
match client.generate_gift_codes(count, dur_type, dur_qty).await {
|
||||
match client
|
||||
.generate_gift_codes(count, duration_type, duration_quantity)
|
||||
.await
|
||||
{
|
||||
Ok(result) => {
|
||||
let codes = result.codes.join(",");
|
||||
flash::redirect_with_flash(
|
||||
|
||||
@@ -43,7 +43,7 @@ pub async fn render(
|
||||
let page = query.members_page.unwrap_or(0);
|
||||
let limit: u32 = 50;
|
||||
let resp = client
|
||||
.list_guild_members(guild_id, limit, page * limit)
|
||||
.list_guild_members(guild_id, limit, u64::from(page) * u64::from(limit))
|
||||
.await
|
||||
.log_error("load guild members")?;
|
||||
Some(tabs::members::members_tab(
|
||||
@@ -57,7 +57,7 @@ pub async fn render(
|
||||
let page = query.reports_page.unwrap_or(0);
|
||||
let limit: u32 = 25;
|
||||
let resp = client
|
||||
.search_reports_by_guild(guild_id, limit, page * limit)
|
||||
.search_reports_by_guild(guild_id, limit, u64::from(page) * u64::from(limit))
|
||||
.await
|
||||
.log_error("load guild reports")?;
|
||||
Some(tabs::reports::reports_tab(
|
||||
|
||||
@@ -82,23 +82,19 @@ async fn guilds_list(
|
||||
}
|
||||
}
|
||||
}
|
||||
Some((guilds, Some(params.requested_ids.len() as u64), false))
|
||||
Some((guilds, params.requested_ids.len() as u64))
|
||||
} else if params.has_search() {
|
||||
let offset = params.page.saturating_mul(params.limit);
|
||||
let offset = u64::from(params.page) * u64::from(params.limit);
|
||||
client
|
||||
.search_guilds(params.search_query(), params.limit, offset)
|
||||
.await
|
||||
.log_error("search guilds")
|
||||
.map(|response| {
|
||||
let has_more = u64::from(offset) + (response.guilds.len() as u64) < response.total;
|
||||
(response.guilds, Some(response.total), has_more)
|
||||
})
|
||||
.map(|response| (response.guilds, response.total))
|
||||
} else {
|
||||
None
|
||||
};
|
||||
let result_guilds = results.as_ref().map(|result| result.0.as_slice());
|
||||
let total = results.as_ref().and_then(|result| result.1);
|
||||
let has_more = results.as_ref().is_some_and(|result| result.2);
|
||||
let total = results.as_ref().map(|result| result.1);
|
||||
|
||||
let markup = templates::pages::guilds_list::guilds_list_page(
|
||||
config,
|
||||
@@ -106,7 +102,6 @@ async fn guilds_list(
|
||||
¶ms,
|
||||
result_guilds,
|
||||
total,
|
||||
has_more,
|
||||
is_results_fragment,
|
||||
);
|
||||
Html(markup.into_string()).into_response()
|
||||
@@ -324,8 +319,12 @@ async fn dispatch_guild_action(
|
||||
"default_message_notifications",
|
||||
"disabled_operations",
|
||||
] {
|
||||
if let Some(v) = form.parse_i64(key) {
|
||||
settings.insert(key.to_string(), serde_json::json!(v));
|
||||
let value = match form.parse_value::<i64>(key) {
|
||||
Ok(value) => value,
|
||||
Err(_) => return FlashData::error(format!("Invalid {key} value")),
|
||||
};
|
||||
if let Some(value) = value {
|
||||
settings.insert(key.to_string(), serde_json::json!(value));
|
||||
}
|
||||
}
|
||||
if form.contains_key("nsfw_submitted") {
|
||||
@@ -356,11 +355,12 @@ async fn dispatch_guild_action(
|
||||
)
|
||||
}
|
||||
"update_disabled_operations" => {
|
||||
let disabled_operations = form
|
||||
.list_values_any(&["disabled_operations[]", "disabled_operations"])
|
||||
.iter()
|
||||
.filter_map(|value| value.parse::<i64>().ok())
|
||||
.fold(0_i64, |acc, value| acc | value);
|
||||
let Ok(operations) =
|
||||
form.parse_list_values::<i64>(&["disabled_operations[]", "disabled_operations"])
|
||||
else {
|
||||
return FlashData::error("Invalid disabled operation value");
|
||||
};
|
||||
let disabled_operations = operations.into_iter().fold(0_i64, |acc, value| acc | value);
|
||||
let settings = serde_json::json!({
|
||||
"disabled_operations": disabled_operations,
|
||||
});
|
||||
|
||||
@@ -21,7 +21,6 @@ use axum::{
|
||||
use serde::Deserialize;
|
||||
|
||||
#[derive(Deserialize)]
|
||||
#[allow(dead_code)]
|
||||
struct JobsQuery {
|
||||
status: Option<String>,
|
||||
task_type: Option<String>,
|
||||
@@ -219,49 +218,36 @@ async fn job_detail_post(
|
||||
}
|
||||
|
||||
fn jobs_url(config: &crate::config::AdminConfig, query: &JobsQuery) -> String {
|
||||
let mut params = Vec::new();
|
||||
push_query(&mut params, "status", query.status.as_deref());
|
||||
push_query(&mut params, "task_type", query.task_type.as_deref());
|
||||
push_query(
|
||||
&mut params,
|
||||
"requested_by_user_id",
|
||||
query.requested_by_user_id.as_deref(),
|
||||
);
|
||||
push_query(
|
||||
&mut params,
|
||||
"max_lookback_days",
|
||||
query.max_lookback_days.as_deref(),
|
||||
);
|
||||
push_query(
|
||||
&mut params,
|
||||
"cursor_bucket_day",
|
||||
query.cursor_bucket_day.as_deref(),
|
||||
);
|
||||
push_query(
|
||||
&mut params,
|
||||
"cursor_created_at",
|
||||
query.cursor_created_at.as_deref(),
|
||||
);
|
||||
push_query(&mut params, "cursor_job_id", query.cursor_job_id.as_deref());
|
||||
if params.is_empty() {
|
||||
return format!("{}/jobs", config.base_path);
|
||||
}
|
||||
let query = params
|
||||
.iter()
|
||||
.map(|(key, value)| {
|
||||
format!(
|
||||
"{}={}",
|
||||
urlencoding::encode(key),
|
||||
urlencoding::encode(value)
|
||||
)
|
||||
})
|
||||
.collect::<Vec<_>>()
|
||||
.join("&");
|
||||
format!("{}/jobs?{query}", config.base_path)
|
||||
}
|
||||
|
||||
fn push_query(params: &mut Vec<(String, String)>, key: &str, value: Option<&str>) {
|
||||
if let Some(value) = value.filter(|value| !value.is_empty()) {
|
||||
params.push((key.to_owned(), value.to_owned()));
|
||||
let query = [
|
||||
("status", query.status.as_deref()),
|
||||
("task_type", query.task_type.as_deref()),
|
||||
(
|
||||
"requested_by_user_id",
|
||||
query.requested_by_user_id.as_deref(),
|
||||
),
|
||||
("max_lookback_days", query.max_lookback_days.as_deref()),
|
||||
("cursor_bucket_day", query.cursor_bucket_day.as_deref()),
|
||||
("cursor_created_at", query.cursor_created_at.as_deref()),
|
||||
("cursor_job_id", query.cursor_job_id.as_deref()),
|
||||
]
|
||||
.into_iter()
|
||||
.filter_map(|(key, value)| {
|
||||
value
|
||||
.filter(|value| !value.is_empty())
|
||||
.map(|value| (key, value))
|
||||
})
|
||||
.map(|(key, value)| {
|
||||
format!(
|
||||
"{}={}",
|
||||
urlencoding::encode(key),
|
||||
urlencoding::encode(value)
|
||||
)
|
||||
})
|
||||
.collect::<Vec<_>>()
|
||||
.join("&");
|
||||
if query.is_empty() {
|
||||
format!("{}/jobs", config.base_path)
|
||||
} else {
|
||||
format!("{}/jobs?{query}", config.base_path)
|
||||
}
|
||||
}
|
||||
@@ -59,53 +59,51 @@ pub(crate) async fn messages_post(
|
||||
match action {
|
||||
"lookup" => {
|
||||
let context_limit = parse_context_limit(form.first("context_limit"));
|
||||
return Redirect::to(&format!(
|
||||
Redirect::to(&format!(
|
||||
"{base}/messages?channel_id={}&message_id={}&context_limit={context_limit}",
|
||||
encode_opt(&channel_id),
|
||||
encode_opt(&message_id)
|
||||
))
|
||||
.into_response();
|
||||
.into_response()
|
||||
}
|
||||
"lookup-by-attachment" => {
|
||||
let attachment_id = form.clean("attachment_id");
|
||||
let filename = form.clean("filename");
|
||||
let context_limit = parse_context_limit(form.first("context_limit"));
|
||||
return Redirect::to(&format!(
|
||||
Redirect::to(&format!(
|
||||
"{base}/messages?channel_id={}&attachment_id={}&filename={}&context_limit={context_limit}",
|
||||
encode_opt(&channel_id),
|
||||
encode_opt(&attachment_id),
|
||||
encode_opt(&filename)
|
||||
))
|
||||
.into_response();
|
||||
}
|
||||
"browse" => {
|
||||
return Redirect::to(&format!(
|
||||
"{base}/messages?channel_id={}",
|
||||
encode_opt(&channel_id)
|
||||
))
|
||||
.into_response();
|
||||
.into_response()
|
||||
}
|
||||
"browse" => Redirect::to(&format!(
|
||||
"{base}/messages?channel_id={}",
|
||||
encode_opt(&channel_id)
|
||||
))
|
||||
.into_response(),
|
||||
"search" => {
|
||||
let search = form.clean("search");
|
||||
return Redirect::to(&format!(
|
||||
Redirect::to(&format!(
|
||||
"{base}/messages?channel_id={}&search={}",
|
||||
encode_opt(&channel_id),
|
||||
encode_opt(&search)
|
||||
))
|
||||
.into_response();
|
||||
.into_response()
|
||||
}
|
||||
"delete" => {
|
||||
let (Some(cid), Some(mid)) = (&channel_id, &message_id) else {
|
||||
return json_error(StatusCode::BAD_REQUEST, "Missing channel_id or message_id");
|
||||
};
|
||||
let audit_log_reason = form.clean("audit_log_reason");
|
||||
return match client
|
||||
match client
|
||||
.delete_message(cid, mid, audit_log_reason.as_deref())
|
||||
.await
|
||||
{
|
||||
Ok(()) => Json(serde_json::json!({"success": true})).into_response(),
|
||||
Err(e) => json_error(StatusCode::BAD_REQUEST, &format!("{e}")),
|
||||
};
|
||||
}
|
||||
}
|
||||
"report-to-ncmec" => {
|
||||
let attachment_id = form.clean("attachment_id");
|
||||
@@ -131,7 +129,7 @@ pub(crate) async fn messages_post(
|
||||
"Missing required NCMEC report fields",
|
||||
);
|
||||
}
|
||||
return match client
|
||||
match client
|
||||
.report_attachment_to_ncmec(
|
||||
cid,
|
||||
mid,
|
||||
@@ -144,11 +142,10 @@ pub(crate) async fn messages_post(
|
||||
{
|
||||
Ok(resp) => Json(resp.data).into_response(),
|
||||
Err(e) => json_error(StatusCode::BAD_REQUEST, &format!("{e}")),
|
||||
};
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
_ => Redirect::to(&format!("{base}/messages")).into_response(),
|
||||
}
|
||||
Redirect::to(&format!("{base}/messages")).into_response()
|
||||
}
|
||||
|
||||
pub(crate) async fn system_dms_post(
|
||||
@@ -253,14 +250,22 @@ pub(crate) async fn bulk_actions_post(
|
||||
}
|
||||
"bulk-schedule-user-deletion" | "bulk_delete_users" => {
|
||||
let user_ids = form.list_values_any(&["user_ids[]", "user_ids"]);
|
||||
let reason_code = form.parse_u32("reason_code").unwrap_or(2);
|
||||
let days = form.parse_u32("days_until_deletion").unwrap_or(14);
|
||||
let (Ok(reason_code), Ok(days)) = (
|
||||
form.parse_value::<u32>("reason_code"),
|
||||
form.parse_value::<u32>("days_until_deletion"),
|
||||
) else {
|
||||
return flash::redirect_with_flash(
|
||||
&format!("{base}/bulk-actions"),
|
||||
FlashData::error("Invalid deletion reason code or delay"),
|
||||
config.secure_cookies(),
|
||||
);
|
||||
};
|
||||
let public_reason = form.clean("public_reason");
|
||||
client
|
||||
.bulk_schedule_user_deletion(
|
||||
&user_ids,
|
||||
reason_code,
|
||||
days,
|
||||
reason_code.unwrap_or(2),
|
||||
days.unwrap_or(14),
|
||||
public_reason.as_deref(),
|
||||
audit_log_reason.as_deref(),
|
||||
)
|
||||
@@ -357,7 +362,7 @@ pub(crate) async fn messages_browse_fragment(
|
||||
}
|
||||
}
|
||||
|
||||
fn parse_context_limit(value: Option<&str>) -> u32 {
|
||||
pub(super) fn parse_context_limit(value: Option<&str>) -> u32 {
|
||||
value
|
||||
.and_then(|s| s.parse::<u32>().ok())
|
||||
.filter(|n| *n > 0)
|
||||
|
||||
@@ -16,7 +16,6 @@ use axum::{
|
||||
use serde::Deserialize;
|
||||
|
||||
#[derive(Deserialize)]
|
||||
#[allow(dead_code)]
|
||||
struct MessagesQuery {
|
||||
channel_id: Option<String>,
|
||||
message_id: Option<String>,
|
||||
@@ -82,13 +81,7 @@ async fn messages_page(
|
||||
let before = query.before.as_deref().filter(|s| !s.is_empty());
|
||||
let after = query.after.as_deref().filter(|s| !s.is_empty());
|
||||
let search = query.search.as_deref().filter(|s| !s.is_empty());
|
||||
let context_limit = query
|
||||
.context_limit
|
||||
.as_deref()
|
||||
.and_then(|s| s.parse::<u32>().ok())
|
||||
.filter(|n| *n > 0)
|
||||
.unwrap_or(50)
|
||||
.min(100);
|
||||
let context_limit = super::message_actions::parse_context_limit(query.context_limit.as_deref());
|
||||
let mut lookup_result = None;
|
||||
let mut browse_result = None;
|
||||
let mut search_result = None;
|
||||
|
||||
@@ -26,7 +26,7 @@ use axum::{
|
||||
};
|
||||
use serde::Deserialize;
|
||||
|
||||
const MAX_REPORT_OFFSET: u32 = 10_000;
|
||||
const MAX_REPORT_OFFSET: u64 = 10_000;
|
||||
|
||||
#[derive(Deserialize)]
|
||||
struct ReportsQuery {
|
||||
@@ -75,7 +75,7 @@ async fn reports_list(
|
||||
let config = state.config();
|
||||
let page = query.page.unwrap_or(0);
|
||||
let limit = query.limit.unwrap_or(25).clamp(1, 200);
|
||||
let offset = page.saturating_mul(limit);
|
||||
let offset = u64::from(page) * u64::from(limit);
|
||||
if offset > MAX_REPORT_OFFSET {
|
||||
return reports_error_page(
|
||||
config,
|
||||
|
||||
@@ -17,14 +17,12 @@ use serde::Deserialize;
|
||||
use super::system_actions;
|
||||
|
||||
#[derive(Deserialize)]
|
||||
#[allow(dead_code)]
|
||||
struct GatewayQuery {
|
||||
leaderboard_limit: Option<String>,
|
||||
node_stats: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
#[allow(dead_code)]
|
||||
struct AuditLogsQuery {
|
||||
q: Option<String>,
|
||||
admin_user_id: Option<String>,
|
||||
@@ -136,7 +134,7 @@ async fn audit_logs_page(
|
||||
sort_by: Some(params.sort_by.to_owned()),
|
||||
sort_order: Some(params.sort_order.to_owned()),
|
||||
limit,
|
||||
offset: current_page * limit,
|
||||
offset: u64::from(current_page) * u64::from(limit),
|
||||
};
|
||||
let result = client
|
||||
.search_audit_logs(&search_params)
|
||||
|
||||
@@ -60,7 +60,9 @@ pub async fn dispatch(
|
||||
"Failed to update user flags",
|
||||
);
|
||||
}
|
||||
let submitted = parse_u64_list(form, &["flags[]", "flags"]);
|
||||
let Ok(submitted) = form.parse_list_values::<u64>(&["flags[]", "flags"]) else {
|
||||
return DispatchOutcome::error("Invalid user flag value");
|
||||
};
|
||||
let selected = submitted.iter().copied().collect::<HashSet<_>>();
|
||||
let user = match client.get_user_by_id(user_id).await {
|
||||
Ok(user) => user,
|
||||
@@ -89,15 +91,22 @@ pub async fn dispatch(
|
||||
}
|
||||
"update_premium_flags" => {
|
||||
if has_legacy_flag_delta_fields(form) {
|
||||
let add = parse_i32_list(form, &["add_flags[]", "add_flags"]);
|
||||
let remove = parse_i32_list(form, &["remove_flags[]", "remove_flags"]);
|
||||
let Ok(add) = form.parse_list_values::<i32>(&["add_flags[]", "add_flags"]) else {
|
||||
return DispatchOutcome::error("Invalid premium flag value to add");
|
||||
};
|
||||
let Ok(remove) = form.parse_list_values::<i32>(&["remove_flags[]", "remove_flags"])
|
||||
else {
|
||||
return DispatchOutcome::error("Invalid premium flag value to remove");
|
||||
};
|
||||
return DispatchOutcome::from_result(
|
||||
client.update_premium_flags(user_id, &add, &remove).await,
|
||||
"Premium flags updated successfully",
|
||||
"Failed to update premium flags",
|
||||
);
|
||||
}
|
||||
let submitted = parse_i32_list(form, &["flags[]", "flags"]);
|
||||
let Ok(submitted) = form.parse_list_values::<i32>(&["flags[]", "flags"]) else {
|
||||
return DispatchOutcome::error("Invalid premium flag value");
|
||||
};
|
||||
let selected = submitted.iter().copied().collect::<HashSet<_>>();
|
||||
let user = match client.get_user_by_id(user_id).await {
|
||||
Ok(user) => user,
|
||||
@@ -124,9 +133,12 @@ pub async fn dispatch(
|
||||
)
|
||||
}
|
||||
"update_suspicious_flags" => {
|
||||
let flags = parse_i32_list(form, &["suspicious_flags[]", "suspicious_flags"])
|
||||
.into_iter()
|
||||
.fold(0, |acc, flag| acc | flag);
|
||||
let Ok(submitted) =
|
||||
form.parse_list_values::<i32>(&["suspicious_flags[]", "suspicious_flags"])
|
||||
else {
|
||||
return DispatchOutcome::error("Invalid suspicious activity flag value");
|
||||
};
|
||||
let flags = submitted.into_iter().fold(0, |acc, flag| acc | flag);
|
||||
DispatchOutcome::from_result(
|
||||
client.update_suspicious_flags(user_id, flags).await,
|
||||
"Suspicious activity flags updated successfully",
|
||||
@@ -225,15 +237,19 @@ pub async fn dispatch(
|
||||
)
|
||||
}
|
||||
"temp_ban" => {
|
||||
let dur = form
|
||||
.parse_u32("duration_hours")
|
||||
.or_else(|| form.parse_u32("duration"))
|
||||
.unwrap_or(24);
|
||||
let Ok(duration) = form.parse_value_any::<u32>(&["duration_hours", "duration"]) else {
|
||||
return DispatchOutcome::error("Invalid ban duration");
|
||||
};
|
||||
let reason = get("reason");
|
||||
let private = get("private_reason");
|
||||
DispatchOutcome::from_result(
|
||||
client
|
||||
.temp_ban_user(user_id, dur, reason.as_deref(), private.as_deref())
|
||||
.temp_ban_user(
|
||||
user_id,
|
||||
duration.unwrap_or(24),
|
||||
reason.as_deref(),
|
||||
private.as_deref(),
|
||||
)
|
||||
.await,
|
||||
"User temporarily banned successfully",
|
||||
"Failed to temporarily ban user",
|
||||
@@ -249,7 +265,7 @@ pub async fn dispatch(
|
||||
return DispatchOutcome::error("IP address is required");
|
||||
};
|
||||
DispatchOutcome::from_result(
|
||||
client.ban_ip(&ip).await,
|
||||
client.ban_ip(&ip, None).await,
|
||||
"IP banned successfully",
|
||||
"Failed to ban IP",
|
||||
)
|
||||
@@ -259,18 +275,29 @@ pub async fn dispatch(
|
||||
return DispatchOutcome::error("Avatar hash is required");
|
||||
};
|
||||
DispatchOutcome::from_result(
|
||||
client.ban_avatar_hash(&hash).await,
|
||||
client.ban_avatar_hash(&hash, None).await,
|
||||
"Avatar hash banned successfully",
|
||||
"Failed to ban avatar hash",
|
||||
)
|
||||
}
|
||||
"schedule_deletion" => {
|
||||
let reason_code = form.parse_i32("reason_code").unwrap_or(0);
|
||||
let Ok(reason_code) = form.parse_value::<i32>("reason_code") else {
|
||||
return DispatchOutcome::error("Invalid deletion reason code");
|
||||
};
|
||||
let public_reason = get("public_reason");
|
||||
let days = form.parse_u32("days_until_deletion").unwrap_or(60);
|
||||
let private_reason = get("private_reason");
|
||||
let Ok(days) = form.parse_value_any::<u32>(&["days_until_deletion", "days"]) else {
|
||||
return DispatchOutcome::error("Invalid deletion delay");
|
||||
};
|
||||
DispatchOutcome::from_result(
|
||||
client
|
||||
.schedule_deletion(user_id, reason_code, public_reason.as_deref(), days)
|
||||
.schedule_deletion(
|
||||
user_id,
|
||||
reason_code.unwrap_or(0),
|
||||
public_reason.as_deref(),
|
||||
days.unwrap_or(60),
|
||||
private_reason.as_deref(),
|
||||
)
|
||||
.await,
|
||||
"User deletion scheduled successfully",
|
||||
"Failed to schedule user deletion",
|
||||
@@ -441,20 +468,6 @@ fn is_relationship_category(category: &str) -> bool {
|
||||
)
|
||||
}
|
||||
|
||||
fn parse_u64_list(form: &MultiValueForm, keys: &[&str]) -> Vec<u64> {
|
||||
form.list_values_any(keys)
|
||||
.iter()
|
||||
.filter_map(|value| value.parse().ok())
|
||||
.collect()
|
||||
}
|
||||
|
||||
fn parse_i32_list(form: &MultiValueForm, keys: &[&str]) -> Vec<i32> {
|
||||
form.list_values_any(keys)
|
||||
.iter()
|
||||
.filter_map(|value| value.parse().ok())
|
||||
.collect()
|
||||
}
|
||||
|
||||
fn parse_dry_run(value: Option<&str>) -> bool {
|
||||
!matches!(value.map(|value| value.trim().to_ascii_lowercase()), Some(value) if value == "false" || value == "0")
|
||||
}
|
||||
|
||||
@@ -164,25 +164,29 @@ pub async fn render(
|
||||
Some(tabs::guilds::guilds_tab(config, user_id, &g))
|
||||
}
|
||||
"reports" => {
|
||||
let lim = query.reports_limit.unwrap_or(25);
|
||||
let sp = query.reports_sent_page.unwrap_or(0);
|
||||
let rp = query.reports_received_page.unwrap_or(0);
|
||||
let limit = query.reports_limit.unwrap_or(25);
|
||||
let sent_page = query.reports_sent_page.unwrap_or(0);
|
||||
let received_page = query.reports_received_page.unwrap_or(0);
|
||||
let sent = client
|
||||
.search_reports_by_reporter(user_id, lim, sp * lim)
|
||||
.search_reports_by_reporter(user_id, limit, u64::from(sent_page) * u64::from(limit))
|
||||
.await
|
||||
.log_error("load reports sent by user");
|
||||
let recv = client
|
||||
.search_reports_by_reported_user(user_id, lim, rp * lim)
|
||||
let received = client
|
||||
.search_reports_by_reported_user(
|
||||
user_id,
|
||||
limit,
|
||||
u64::from(received_page) * u64::from(limit),
|
||||
)
|
||||
.await
|
||||
.log_error("load reports against user");
|
||||
Some(tabs::reports::reports_tab(
|
||||
config,
|
||||
user_id,
|
||||
sent.as_ref(),
|
||||
recv.as_ref(),
|
||||
sp,
|
||||
rp,
|
||||
lim,
|
||||
received.as_ref(),
|
||||
sent_page,
|
||||
received_page,
|
||||
limit,
|
||||
))
|
||||
}
|
||||
"relationships" => {
|
||||
@@ -244,7 +248,7 @@ pub async fn render(
|
||||
sort_by: Some("created_at".to_owned()),
|
||||
sort_order: Some("desc".to_owned()),
|
||||
limit,
|
||||
offset: page * limit,
|
||||
offset: u64::from(page) * u64::from(limit),
|
||||
})
|
||||
.await
|
||||
.log_error("load user admin audit logs")?;
|
||||
|
||||
@@ -93,7 +93,7 @@ async fn users_list(
|
||||
.log_error("lookup users by ids")
|
||||
.map(|users| (users, false))
|
||||
} else if params.has_search() {
|
||||
let offset = params.page.saturating_mul(params.limit);
|
||||
let offset = u64::from(params.page) * u64::from(params.limit);
|
||||
client
|
||||
.search_users(
|
||||
params.search_query(),
|
||||
@@ -105,7 +105,7 @@ async fn users_list(
|
||||
.await
|
||||
.log_error("search users")
|
||||
.map(|r| {
|
||||
let has_more = u64::from(offset) + (r.users.len() as u64) < r.total;
|
||||
let has_more = (r.users.len() as u64) < r.total.saturating_sub(offset);
|
||||
(r.users, has_more)
|
||||
})
|
||||
} else {
|
||||
|
||||
Reference in new issue
Block a user