mirror of
https://github.com/storytold/artcraft.git
synced 2026-10-21 09:22:41 -04:00
Compare commits
2
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2f38d71e8e | ||
|
|
079ccacd51 |
+2
-2
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"db_name": "MySQL",
|
||||
"query": "\nUPDATE users\nSET\n is_banned = ?,\n maybe_mod_comments = ?,\n maybe_mod_user_token = ?,\n version = version + 1\n\nWHERE users.token = ?\nLIMIT 1\n ",
|
||||
"query": "\nUPDATE users\nSET\n is_banned = ?,\n maybe_mod_comments = ?,\n maybe_mod_user_token = ?,\n version = version + 1\n\nWHERE users.token = ?\nLIMIT 1\n ",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"parameters": {
|
||||
@@ -8,5 +8,5 @@
|
||||
},
|
||||
"nullable": []
|
||||
},
|
||||
"hash": "eab69a8d679b2dcfb06a9ccc79851f6c693d0c372142ff0e7ded89c95e4bcf44"
|
||||
"hash": "3d746dcb479005f1f4ae5e157a5ace7626eda4849d44c6c2d7079fd346eb4edc"
|
||||
}
|
||||
+24
-20
@@ -1,20 +1,27 @@
|
||||
use anyhow::anyhow;
|
||||
use sqlx::MySqlPool;
|
||||
use std::marker::PhantomData;
|
||||
|
||||
use sqlx::{Executor, MySql};
|
||||
|
||||
use errors::AnyhowResult;
|
||||
use tokens::tokens::users::UserToken;
|
||||
|
||||
pub struct SetUserBanStatsArgs<'a> {
|
||||
pub struct SetUserBanStatusArgs<'a, 'c: 'a, E>
|
||||
where E: 'a + Executor<'c, Database = MySql>
|
||||
{
|
||||
pub subject_user_token: &'a UserToken,
|
||||
pub is_banned: bool,
|
||||
pub mod_user_token: &'a UserToken,
|
||||
pub maybe_mod_comments: Option<&'a str>,
|
||||
pub mysql_pool: &'a MySqlPool,
|
||||
pub mysql_executor: E,
|
||||
pub phantom: PhantomData<&'c E>,
|
||||
}
|
||||
|
||||
pub async fn set_user_ban_status(args: SetUserBanStatsArgs<'_>) -> AnyhowResult<()> {
|
||||
let query_result = sqlx::query!(
|
||||
r#"
|
||||
pub async fn set_user_ban_status<'a, 'c, E>(
|
||||
args: SetUserBanStatusArgs<'a, 'c, E>,
|
||||
) -> Result<(), sqlx::Error>
|
||||
where E: 'a + Executor<'c, Database = MySql>
|
||||
{
|
||||
sqlx::query!(
|
||||
r#"
|
||||
UPDATE users
|
||||
SET
|
||||
is_banned = ?,
|
||||
@@ -24,17 +31,14 @@ SET
|
||||
|
||||
WHERE users.token = ?
|
||||
LIMIT 1
|
||||
"#,
|
||||
args.is_banned,
|
||||
args.maybe_mod_comments,
|
||||
args.mod_user_token,
|
||||
args.subject_user_token,
|
||||
)
|
||||
.execute(args.mysql_pool)
|
||||
.await;
|
||||
"#,
|
||||
args.is_banned,
|
||||
args.maybe_mod_comments,
|
||||
args.mod_user_token,
|
||||
args.subject_user_token,
|
||||
)
|
||||
.execute(args.mysql_executor)
|
||||
.await?;
|
||||
|
||||
match query_result {
|
||||
Ok(_) => Ok(()),
|
||||
Err(err) => Err(anyhow!("error with query: {:?}", err)),
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -20,6 +20,14 @@ pub enum StaffAuditAction {
|
||||
/// Staff redeemed an impersonation request, creating a session.
|
||||
#[serde(rename = "impersonate_user_redeem")]
|
||||
ImpersonateUserRedeem,
|
||||
|
||||
/// Staff banned a user.
|
||||
#[serde(rename = "ban_user")]
|
||||
BanUser,
|
||||
|
||||
/// Staff unbanned a user.
|
||||
#[serde(rename = "unban_user")]
|
||||
UnbanUser,
|
||||
}
|
||||
|
||||
impl_enum_display_and_debug_using_to_str!(StaffAuditAction);
|
||||
@@ -31,6 +39,8 @@ impl StaffAuditAction {
|
||||
match self {
|
||||
Self::ImpersonateUserRequest => "impersonate_user_request",
|
||||
Self::ImpersonateUserRedeem => "impersonate_user_redeem",
|
||||
Self::BanUser => "ban_user",
|
||||
Self::UnbanUser => "unban_user",
|
||||
}
|
||||
}
|
||||
|
||||
@@ -38,6 +48,8 @@ impl StaffAuditAction {
|
||||
match value {
|
||||
"impersonate_user_request" => Ok(Self::ImpersonateUserRequest),
|
||||
"impersonate_user_redeem" => Ok(Self::ImpersonateUserRedeem),
|
||||
"ban_user" => Ok(Self::BanUser),
|
||||
"unban_user" => Ok(Self::UnbanUser),
|
||||
_ => Err(format!("invalid StaffAuditAction value: {:?}", value)),
|
||||
}
|
||||
}
|
||||
@@ -46,6 +58,8 @@ impl StaffAuditAction {
|
||||
BTreeSet::from([
|
||||
Self::ImpersonateUserRequest,
|
||||
Self::ImpersonateUserRedeem,
|
||||
Self::BanUser,
|
||||
Self::UnbanUser,
|
||||
])
|
||||
}
|
||||
}
|
||||
@@ -62,24 +76,30 @@ mod tests {
|
||||
fn test_serialization() {
|
||||
assert_serialization(StaffAuditAction::ImpersonateUserRequest, "impersonate_user_request");
|
||||
assert_serialization(StaffAuditAction::ImpersonateUserRedeem, "impersonate_user_redeem");
|
||||
assert_serialization(StaffAuditAction::BanUser, "ban_user");
|
||||
assert_serialization(StaffAuditAction::UnbanUser, "unban_user");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn to_str() {
|
||||
assert_eq!(StaffAuditAction::ImpersonateUserRequest.to_str(), "impersonate_user_request");
|
||||
assert_eq!(StaffAuditAction::ImpersonateUserRedeem.to_str(), "impersonate_user_redeem");
|
||||
assert_eq!(StaffAuditAction::BanUser.to_str(), "ban_user");
|
||||
assert_eq!(StaffAuditAction::UnbanUser.to_str(), "unban_user");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn from_str() {
|
||||
assert_eq!(StaffAuditAction::from_str("impersonate_user_request").unwrap(), StaffAuditAction::ImpersonateUserRequest);
|
||||
assert_eq!(StaffAuditAction::from_str("impersonate_user_redeem").unwrap(), StaffAuditAction::ImpersonateUserRedeem);
|
||||
assert_eq!(StaffAuditAction::from_str("ban_user").unwrap(), StaffAuditAction::BanUser);
|
||||
assert_eq!(StaffAuditAction::from_str("unban_user").unwrap(), StaffAuditAction::UnbanUser);
|
||||
assert!(StaffAuditAction::from_str("invalid").is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn all_variants() {
|
||||
const EXPECTED_COUNT: usize = 2;
|
||||
const EXPECTED_COUNT: usize = 4;
|
||||
assert_eq!(StaffAuditAction::all_variants().len(), EXPECTED_COUNT);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -87,6 +87,7 @@ use crate::http_server::endpoints::media_files::upsert_upload::write_scene_file:
|
||||
use crate::http_server::endpoints::model_download::enqueue_gptsovits_model_download_handler::*;
|
||||
use crate::http_server::endpoints::moderation::user_feature_flags::edit_user_feature_flags_handler::*;
|
||||
use crate::http_server::endpoints::moderation::staff_audit_logs::moderator_list_staff_audit_logs_handler::*;
|
||||
use crate::http_server::endpoints::moderation::user_bans::moderation_ban_user_handler::*;
|
||||
use crate::http_server::endpoints::moderation::user_sessions::moderator_list_user_session_impersonation_requests_for_user_handler::*;
|
||||
use crate::http_server::endpoints::moderation::user_sessions::moderator_list_user_session_impersonation_requests_handler::*;
|
||||
use crate::http_server::endpoints::moderation::user_sessions::moderator_user_session_impersonation_request_handler::*;
|
||||
@@ -512,6 +513,7 @@ use crate::http_server::endpoints::media_files::list::list_batch_generated_redux
|
||||
crate::http_server::endpoints::moderation::wallets::moderator_create_wallet_for_user_handler::moderator_create_wallet_for_user_handler,
|
||||
crate::http_server::endpoints::moderation::wallets::moderator_get_wallet_handler::moderator_get_wallet_handler,
|
||||
crate::http_server::endpoints::moderation::staff_audit_logs::moderator_list_staff_audit_logs_handler::moderator_list_staff_audit_logs_handler,
|
||||
crate::http_server::endpoints::moderation::user_bans::moderation_ban_user_handler::moderation_ban_user_handler,
|
||||
crate::http_server::endpoints::moderation::user_sessions::moderator_list_user_session_impersonation_requests_for_user_handler::moderator_list_user_session_impersonation_requests_for_user_handler,
|
||||
crate::http_server::endpoints::moderation::user_sessions::moderator_list_user_session_impersonation_requests_handler::moderator_list_user_session_impersonation_requests_handler,
|
||||
crate::http_server::endpoints::moderation::user_sessions::moderator_user_session_impersonation_request_handler::moderator_user_session_impersonation_request_handler,
|
||||
@@ -947,6 +949,8 @@ use crate::http_server::endpoints::media_files::list::list_batch_generated_redux
|
||||
MediaFilesByBatchListItem,
|
||||
ModelWeightForList,
|
||||
ModelWeightSearchResult,
|
||||
ModerationBanUserRequest,
|
||||
ModerationBanUserSuccessResponse,
|
||||
ModerationImpersonateRequest,
|
||||
ModerationImpersonateSuccessResponse,
|
||||
ModernInferenceQueueStats,
|
||||
|
||||
-142
@@ -1,142 +0,0 @@
|
||||
use std::fmt;
|
||||
use std::sync::Arc;
|
||||
|
||||
use actix_web::error::ResponseError;
|
||||
use actix_web::http::StatusCode;
|
||||
use actix_web::{web, HttpRequest, HttpResponse};
|
||||
use log::warn;
|
||||
|
||||
use mysql_queries::queries::users::user::update::set_user_ban_status::{set_user_ban_status, SetUserBanStatsArgs};
|
||||
use mysql_queries::queries::users::user_profiles::get_user_profile_by_username::get_user_profile_by_username;
|
||||
|
||||
use crate::http_server::web_utils::response_success_helpers::simple_json_success;
|
||||
use crate::http_server::web_utils::serialize_as_json_error::serialize_as_json_error;
|
||||
use crate::state::server_state::ServerState;
|
||||
|
||||
#[derive(Deserialize)]
|
||||
pub struct BanUserRequest {
|
||||
pub username: String,
|
||||
pub mod_notes: String,
|
||||
pub is_banned: bool,
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
pub struct BanUserSuccessResponse {
|
||||
pub success: bool,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Debug)]
|
||||
pub struct BanUserErrorResponse {
|
||||
pub success: bool,
|
||||
pub error_type: BanUserErrorType,
|
||||
pub error_message: String,
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Debug, Serialize)]
|
||||
pub enum BanUserErrorType {
|
||||
ServerError,
|
||||
Unauthorized,
|
||||
UserNotFound,
|
||||
}
|
||||
|
||||
impl BanUserErrorResponse {
|
||||
fn unauthorized() -> Self {
|
||||
Self {
|
||||
success: false,
|
||||
error_type: BanUserErrorType::Unauthorized,
|
||||
error_message: "unauthorized".to_string()
|
||||
}
|
||||
}
|
||||
fn server_error() -> Self {
|
||||
Self {
|
||||
success: false,
|
||||
error_type: BanUserErrorType::ServerError,
|
||||
error_message: "server error".to_string()
|
||||
}
|
||||
}
|
||||
fn not_found() -> Self {
|
||||
Self {
|
||||
success: false,
|
||||
error_type: BanUserErrorType::UserNotFound,
|
||||
error_message: "user not found".to_string()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl ResponseError for BanUserErrorResponse {
|
||||
fn status_code(&self) -> StatusCode {
|
||||
match self.error_type {
|
||||
BanUserErrorType::ServerError => StatusCode::INTERNAL_SERVER_ERROR,
|
||||
BanUserErrorType::Unauthorized => StatusCode::UNAUTHORIZED,
|
||||
BanUserErrorType::UserNotFound => StatusCode::NOT_FOUND,
|
||||
}
|
||||
}
|
||||
|
||||
fn error_response(&self) -> HttpResponse {
|
||||
serialize_as_json_error(self)
|
||||
}
|
||||
}
|
||||
|
||||
// NB: Not using DeriveMore since Clion doesn't understand it.
|
||||
impl fmt::Display for BanUserErrorResponse {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
write!(f, "{:?}", self.error_type)
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn ban_user_handler(
|
||||
http_request: HttpRequest,
|
||||
request: web::Json<BanUserRequest>,
|
||||
server_state: web::Data<Arc<ServerState>>
|
||||
) -> Result<HttpResponse, BanUserErrorResponse> {
|
||||
|
||||
let maybe_user_session = server_state
|
||||
.session_checker
|
||||
.maybe_get_user_session(&http_request, &server_state.mysql_pool)
|
||||
.await
|
||||
.map_err(|e| {
|
||||
warn!("Session checker error: {:?}", e);
|
||||
BanUserErrorResponse::server_error()
|
||||
})?;
|
||||
|
||||
let user_session = match maybe_user_session {
|
||||
Some(session) => session,
|
||||
None => {
|
||||
warn!("not logged in");
|
||||
return Err(BanUserErrorResponse::unauthorized());
|
||||
}
|
||||
};
|
||||
|
||||
if !user_session.can_ban_users {
|
||||
warn!("user is not allowed to add bans: {:?}", user_session.user_token);
|
||||
return Err(BanUserErrorResponse::unauthorized());
|
||||
}
|
||||
|
||||
let username_lower = request.username.to_lowercase();
|
||||
|
||||
let user_lookup_result = get_user_profile_by_username(
|
||||
&username_lower, &server_state.mysql_pool).await;
|
||||
|
||||
let user_profile = match user_lookup_result {
|
||||
Ok(Some(result)) => result,
|
||||
Ok(None) => return Err(BanUserErrorResponse::not_found()),
|
||||
Err(err) => {
|
||||
warn!("lookup error: {:?}", err);
|
||||
return Err(BanUserErrorResponse::server_error());
|
||||
}
|
||||
};
|
||||
|
||||
set_user_ban_status(SetUserBanStatsArgs {
|
||||
subject_user_token: &user_profile.user_token,
|
||||
is_banned: request.is_banned,
|
||||
mod_user_token: &user_session.user_token,
|
||||
maybe_mod_comments: Some(&request.mod_notes),
|
||||
mysql_pool: &server_state.mysql_pool,
|
||||
}).await
|
||||
.map_err(|err| {
|
||||
warn!("Add IP ban DB error: {:?}", err);
|
||||
BanUserErrorResponse::server_error()
|
||||
})?;
|
||||
|
||||
Ok(simple_json_success())
|
||||
}
|
||||
+1
-1
@@ -1 +1 @@
|
||||
pub mod ban_user;
|
||||
pub mod moderation_ban_user_handler;
|
||||
+158
@@ -0,0 +1,158 @@
|
||||
use std::marker::PhantomData;
|
||||
use std::sync::Arc;
|
||||
|
||||
use actix_web::web::Json;
|
||||
use actix_web::{web, HttpRequest};
|
||||
use log::{info, warn};
|
||||
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 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::update::set_user_ban_status::{
|
||||
set_user_ban_status, SetUserBanStatusArgs,
|
||||
};
|
||||
use mysql_queries::queries::users::user_profiles::get_user_profile_by_username::get_user_profile_by_username;
|
||||
|
||||
use crate::http_server::common_responses::advanced_common_web_error::AdvancedCommonWebError;
|
||||
use crate::http_server::web_utils::user_session::require_moderator::{
|
||||
require_moderator, UseDatabase,
|
||||
};
|
||||
use crate::state::server_state::ServerState;
|
||||
|
||||
// --- Request ---
|
||||
|
||||
#[derive(Deserialize, ToSchema)]
|
||||
pub struct ModerationBanUserRequest {
|
||||
pub username: String,
|
||||
pub mod_notes: String,
|
||||
pub is_banned: bool,
|
||||
}
|
||||
|
||||
// --- Response ---
|
||||
|
||||
#[derive(Serialize, ToSchema)]
|
||||
pub struct ModerationBanUserSuccessResponse {
|
||||
pub success: bool,
|
||||
}
|
||||
|
||||
// --- Handler ---
|
||||
|
||||
/// Ban or unban a user. Moderators only.
|
||||
#[utoipa::path(
|
||||
post,
|
||||
tag = "Moderation",
|
||||
path = "/moderation/user_bans/manage_ban",
|
||||
request_body = ModerationBanUserRequest,
|
||||
responses(
|
||||
(status = 200, description = "User ban status updated", body = ModerationBanUserSuccessResponse),
|
||||
(status = 401, description = "Unauthorized"),
|
||||
(status = 404, description = "User not found"),
|
||||
(status = 500, description = "Server error"),
|
||||
),
|
||||
)]
|
||||
pub async fn moderation_ban_user_handler(
|
||||
http_request: HttpRequest,
|
||||
request: Json<ModerationBanUserRequest>,
|
||||
server_state: web::Data<Arc<ServerState>>,
|
||||
) -> Result<Json<ModerationBanUserSuccessResponse>, AdvancedCommonWebError> {
|
||||
|
||||
// 1. Require moderator with ban permissions.
|
||||
let user_session = require_moderator(
|
||||
&http_request,
|
||||
&server_state,
|
||||
UseDatabase::GrabNewConnection,
|
||||
).await.map_err(|err| {
|
||||
warn!("Moderator check failed: {:?}", err);
|
||||
AdvancedCommonWebError::NotAuthorized
|
||||
})?;
|
||||
|
||||
if !user_session.can_ban_users {
|
||||
warn!("User {} is not allowed to ban users", user_session.user_token.as_str());
|
||||
return Err(AdvancedCommonWebError::NotAuthorized);
|
||||
}
|
||||
|
||||
// 2. Look up the target user by username.
|
||||
let username_lower = request.username.to_lowercase();
|
||||
|
||||
let user_profile = get_user_profile_by_username(
|
||||
&username_lower,
|
||||
&server_state.mysql_pool,
|
||||
).await.map_err(|err| {
|
||||
warn!("User lookup error: {:?}", err);
|
||||
AdvancedCommonWebError::from_anyhow_error(err)
|
||||
})?.ok_or_else(|| {
|
||||
warn!("User not found for ban: {}", username_lower);
|
||||
AdvancedCommonWebError::NotFound
|
||||
})?;
|
||||
|
||||
let ip_address = get_request_ip(&http_request);
|
||||
|
||||
let audit_action = if request.is_banned {
|
||||
StaffAuditAction::BanUser
|
||||
} else {
|
||||
StaffAuditAction::UnbanUser
|
||||
};
|
||||
|
||||
info!(
|
||||
"Moderator {} {} user {} ({})",
|
||||
user_session.user_token.as_str(),
|
||||
audit_action.to_str(),
|
||||
user_profile.user_token.as_str(),
|
||||
username_lower,
|
||||
);
|
||||
|
||||
// 3. Begin transaction: ban + audit log.
|
||||
let mut transaction = server_state.mysql_pool.begin().await
|
||||
.map_err(|err| {
|
||||
warn!("Failed to begin transaction: {:?}", err);
|
||||
AdvancedCommonWebError::from_error(err)
|
||||
})?;
|
||||
|
||||
// 4. Set ban status.
|
||||
set_user_ban_status(SetUserBanStatusArgs {
|
||||
subject_user_token: &user_profile.user_token,
|
||||
is_banned: request.is_banned,
|
||||
mod_user_token: &user_session.user_token,
|
||||
maybe_mod_comments: Some(&request.mod_notes),
|
||||
mysql_executor: &mut *transaction,
|
||||
phantom: PhantomData,
|
||||
}).await.map_err(|err| {
|
||||
warn!("Failed to set user ban status: {:?}", err);
|
||||
AdvancedCommonWebError::from_error(err)
|
||||
})?;
|
||||
|
||||
// 5. Insert staff audit log.
|
||||
let _audit_token = insert_staff_audit_log(InsertStaffAuditLogArgs {
|
||||
audit_action,
|
||||
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);
|
||||
AdvancedCommonWebError::from_error(err)
|
||||
})?;
|
||||
|
||||
// 6. Commit transaction.
|
||||
transaction.commit().await.map_err(|err| {
|
||||
warn!("Failed to commit transaction: {:?}", err);
|
||||
AdvancedCommonWebError::from_error(err)
|
||||
})?;
|
||||
|
||||
info!(
|
||||
"User {} ban status set to {} by moderator {}",
|
||||
user_profile.user_token.as_str(),
|
||||
request.is_banned,
|
||||
user_session.user_token.as_str(),
|
||||
);
|
||||
|
||||
Ok(Json(ModerationBanUserSuccessResponse {
|
||||
success: true,
|
||||
}))
|
||||
}
|
||||
+2
-2
@@ -29,7 +29,7 @@ use crate::http_server::endpoints::moderation::wallets::moderator_get_wallet_han
|
||||
use crate::http_server::endpoints::moderation::jobs::get_tts_inference_queue_count::get_tts_inference_queue_count_handler;
|
||||
use crate::http_server::endpoints::moderation::jobs::get_w2l_inference_queue_count::get_w2l_inference_queue_count_handler;
|
||||
use crate::http_server::endpoints::moderation::jobs::kill_tts_inference_jobs::kill_tts_inference_jobs_handler;
|
||||
use crate::http_server::endpoints::moderation::user_bans::ban_user::ban_user_handler;
|
||||
use crate::http_server::endpoints::moderation::user_bans::moderation_ban_user_handler::moderation_ban_user_handler;
|
||||
use crate::http_server::endpoints::moderation::user::moderator_list_subscribing_users_by_signup_date::moderator_list_subscribing_users_by_signup_date_handler;
|
||||
use crate::http_server::endpoints::moderation::user::moderator_list_users_by_signup_date::moderator_list_users_by_signup_date_handler;
|
||||
use crate::http_server::endpoints::moderation::user::moderator_user_lookup_by_stripe_customer_id_handler::moderator_user_lookup_by_stripe_customer_id_handler;
|
||||
@@ -183,7 +183,7 @@ pub fn add_moderator_routes<T, B> (app: App<T>) -> App<T>
|
||||
web::scope("/user_bans")
|
||||
.service(
|
||||
web::resource("/manage_ban")
|
||||
.route(web::post().to(ban_user_handler))
|
||||
.route(web::post().to(moderation_ban_user_handler))
|
||||
.route(web::head().to(|| HttpResponse::Ok()))
|
||||
)
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user