@@ -25,6 +25,7 @@ use std::sync::Arc;
2525use uuid:: Uuid ;
2626
2727use crate :: { middleware:: AuthContext , AppState } ;
28+ use crate :: middleware:: { HeaderMap , check_rate_limit_with_fallback, extract_client_ip, USER_AGENT } ;
2829
2930/// All moderation routes (require authentication).
3031pub fn router ( ) -> Router < Arc < AppState > > {
@@ -179,6 +180,21 @@ async fn kick_member(
179180 require_server_permission ( & state. db . pool , & server, auth. user_id , Permissions :: KICK_MEMBERS )
180181 . await ?;
181182
183+ // Rate limiting: 10 kicks per user per 5 minutes
184+ let ip = extract_client_ip ( & headers) ;
185+ check_rate_limit_with_fallback (
186+ state. db . redis . as_ref ( ) ,
187+ format ! ( "rl:kick:user:{}" , auth. user_id) ,
188+ 10 ,
189+ 300 ,
190+ ) . await ?;
191+ check_rate_limit_with_fallback (
192+ state. db . redis . as_ref ( ) ,
193+ format ! ( "rl:kick:ip:{ip}" ) ,
194+ 20 ,
195+ 300 ,
196+ ) . await ?;
197+
182198 // Can't kick the owner
183199 if target_id == server. owner_id {
184200 return Err ( NexusError :: Forbidden ) ;
@@ -245,6 +261,21 @@ async fn ban_member(
245261 require_server_permission ( & state. db . pool , & server, auth. user_id , Permissions :: BAN_MEMBERS )
246262 . await ?;
247263
264+ // Rate limiting: 10 bans per user per 5 minutes
265+ let ip = extract_client_ip ( & headers) ;
266+ check_rate_limit_with_fallback (
267+ state. db . redis . as_ref ( ) ,
268+ format ! ( "rl:ban:user:{}" , auth. user_id) ,
269+ 10 ,
270+ 300 ,
271+ ) . await ?;
272+ check_rate_limit_with_fallback (
273+ state. db . redis . as_ref ( ) ,
274+ format ! ( "rl:ban:ip:{ip}" ) ,
275+ 20 ,
276+ 300 ,
277+ ) . await ?;
278+
248279 if target_id == server. owner_id {
249280 return Err ( NexusError :: Forbidden ) ;
250281 }
@@ -307,6 +338,21 @@ async fn unban_member(
307338 require_server_permission ( & state. db . pool , & server, auth. user_id , Permissions :: BAN_MEMBERS )
308339 . await ?;
309340
341+ // Rate limiting: 10 unbans per user per 5 minutes
342+ let ip = extract_client_ip ( & headers) ;
343+ check_rate_limit_with_fallback (
344+ state. db . redis . as_ref ( ) ,
345+ format ! ( "rl:unban:user:{}" , auth. user_id) ,
346+ 10 ,
347+ 300 ,
348+ ) . await ?;
349+ check_rate_limit_with_fallback (
350+ state. db . redis . as_ref ( ) ,
351+ format ! ( "rl:unban:ip:{ip}" ) ,
352+ 20 ,
353+ 300 ,
354+ ) . await ?;
355+
310356 let removed = moderation:: remove_ban ( & state. db . pool , target_id, server_id) . await ?;
311357 if !removed {
312358 return Err ( NexusError :: NotFound { resource : "Ban" . into ( ) } ) ;
@@ -376,6 +422,21 @@ async fn set_timeout(
376422 require_server_permission ( & state. db . pool , & server, auth. user_id , Permissions :: KICK_MEMBERS )
377423 . await ?;
378424
425+ // Rate limiting: 10 timeouts per user per 5 minutes
426+ let ip = extract_client_ip ( & headers) ;
427+ check_rate_limit_with_fallback (
428+ state. db . redis . as_ref ( ) ,
429+ format ! ( "rl:timeout:user:{}" , auth. user_id) ,
430+ 10 ,
431+ 300 ,
432+ ) . await ?;
433+ check_rate_limit_with_fallback (
434+ state. db . redis . as_ref ( ) ,
435+ format ! ( "rl:timeout:ip:{ip}" ) ,
436+ 20 ,
437+ 300 ,
438+ ) . await ?;
439+
379440 if target_id == server. owner_id {
380441 return Err ( NexusError :: Forbidden ) ;
381442 }
@@ -601,6 +662,21 @@ async fn resolve_report(
601662 Path ( ( server_id, report_id) ) : Path < ( Uuid , Uuid ) > ,
602663 Json ( body) : Json < ResolveReportBody > ,
603664) -> NexusResult < Json < serde_json:: Value > > {
665+ // Rate limiting: 20 report resolutions per user per 5 minutes
666+ let ip = extract_client_ip ( & headers) ;
667+ check_rate_limit_with_fallback (
668+ state. db . redis . as_ref ( ) ,
669+ format ! ( "rl:report_resolve:user:{}" , auth. user_id) ,
670+ 20 ,
671+ 300 ,
672+ ) . await ?;
673+ check_rate_limit_with_fallback (
674+ state. db . redis . as_ref ( ) ,
675+ format ! ( "rl:report_resolve:ip:{ip}" ) ,
676+ 40 ,
677+ 300 ,
678+ ) . await ?;
679+
604680 let server = get_server_or_404 ( & state. db . pool , server_id) . await ?;
605681 require_server_permission (
606682 & state. db . pool ,
@@ -642,6 +718,21 @@ async fn dismiss_report(
642718 State ( state) : State < Arc < AppState > > ,
643719 Path ( ( server_id, report_id) ) : Path < ( Uuid , Uuid ) > ,
644720) -> NexusResult < Json < serde_json:: Value > > {
721+ // Rate limiting: 20 report dismissals per user per 5 minutes
722+ let ip = extract_client_ip ( & headers) ;
723+ check_rate_limit_with_fallback (
724+ state. db . redis . as_ref ( ) ,
725+ format ! ( "rl:report_dismiss:user:{}" , auth. user_id) ,
726+ 20 ,
727+ 300 ,
728+ ) . await ?;
729+ check_rate_limit_with_fallback (
730+ state. db . redis . as_ref ( ) ,
731+ format ! ( "rl:report_dismiss:ip:{ip}" ) ,
732+ 40 ,
733+ 300 ,
734+ ) . await ?;
735+
645736 let server = get_server_or_404 ( & state. db . pool , server_id) . await ?;
646737 require_server_permission (
647738 & state. db . pool ,
@@ -708,6 +799,21 @@ async fn add_word_filter(
708799 Path ( server_id) : Path < Uuid > ,
709800 Json ( body) : Json < AddFilterBody > ,
710801) -> NexusResult < Json < moderation:: WordFilter > > {
802+ // Rate limiting: 10 word filter changes per user per 5 minutes
803+ let ip = extract_client_ip ( & headers) ;
804+ check_rate_limit_with_fallback (
805+ state. db . redis . as_ref ( ) ,
806+ format ! ( "rl:word_filter:user:{}" , auth. user_id) ,
807+ 10 ,
808+ 300 ,
809+ ) . await ?;
810+ check_rate_limit_with_fallback (
811+ state. db . redis . as_ref ( ) ,
812+ format ! ( "rl:word_filter:ip:{ip}" ) ,
813+ 20 ,
814+ 300 ,
815+ ) . await ?;
816+
711817 let pattern = body. pattern . trim ( ) . to_lowercase ( ) ;
712818 if pattern. is_empty ( ) {
713819 return Err ( NexusError :: Validation { message : "Pattern cannot be empty" . into ( ) } ) ;
@@ -771,6 +877,21 @@ async fn remove_word_filter(
771877 State ( state) : State < Arc < AppState > > ,
772878 Path ( ( server_id, filter_id) ) : Path < ( Uuid , Uuid ) > ,
773879) -> NexusResult < Json < serde_json:: Value > > {
880+ // Rate limiting: 10 word filter changes per user per 5 minutes
881+ let ip = extract_client_ip ( & headers) ;
882+ check_rate_limit_with_fallback (
883+ state. db . redis . as_ref ( ) ,
884+ format ! ( "rl:word_filter:user:{}" , auth. user_id) ,
885+ 10 ,
886+ 300 ,
887+ ) . await ?;
888+ check_rate_limit_with_fallback (
889+ state. db . redis . as_ref ( ) ,
890+ format ! ( "rl:word_filter:ip:{ip}" ) ,
891+ 20 ,
892+ 300 ,
893+ ) . await ?;
894+
774895 let server = get_server_or_404 ( & state. db . pool , server_id) . await ?;
775896 require_server_permission ( & state. db . pool , & server, auth. user_id , Permissions :: MANAGE_SERVER )
776897 . await ?;
0 commit comments