auth_framework/api/
security_simple.rs1use crate::api::{ApiResponse, ApiState};
8use axum::{
9 Form,
10 extract::{Path, State},
11};
12use lazy_static::lazy_static;
13use serde::{Deserialize, Serialize};
14use serde_json::json;
15use std::collections::HashSet;
16use std::net::IpAddr;
17use std::sync::RwLock;
18
19lazy_static! {
21 static ref IP_BLACKLIST: RwLock<HashSet<IpAddr>> = RwLock::new(HashSet::new());
22 static ref SECURITY_STATS: RwLock<SecurityStats> = RwLock::new(SecurityStats::default());
23}
24
25#[derive(Debug, Default, Clone, Serialize)]
26struct SecurityStats {
27 blocked_requests: u64,
28 failed_auth_attempts: u64,
29 suspicious_activity: u64,
30 last_updated: Option<i64>,
31}
32
33#[derive(Debug, Deserialize)]
34pub struct BlacklistIpForm {
35 pub ip: String,
36 pub reason: Option<String>,
37}
38
39#[derive(Debug, Serialize)]
40pub struct SecurityStatsResponse {
41 pub blocked_requests: u64,
42 pub failed_auth_attempts: u64,
43 pub suspicious_activity: u64,
44 pub blacklisted_ips: usize,
45 pub last_updated: Option<i64>,
46}
47
48pub async fn blacklist_ip_endpoint(
51 State(_state): State<ApiState>,
52 Form(form): Form<BlacklistIpForm>,
53) -> ApiResponse<serde_json::Value> {
54 let ip: IpAddr = match form.ip.parse() {
55 Ok(ip) => ip,
56 Err(_) => return ApiResponse::error_typed("invalid_ip", "Invalid IP address format"),
57 };
58
59 {
60 let Ok(mut blacklist) = IP_BLACKLIST.write() else {
61 return ApiResponse::error_typed("internal_error", "Security subsystem unavailable");
62 };
63 blacklist.insert(ip);
64 }
65
66 {
68 if let Ok(mut stats) = SECURITY_STATS.write() {
69 stats.blocked_requests += 1;
70 stats.last_updated = Some(chrono::Utc::now().timestamp());
71 }
72 }
73
74 let data = json!({
75 "ip": ip.to_string(),
76 "reason": form.reason.unwrap_or_else(|| "Manual blacklist".to_string())
77 });
78
79 ApiResponse::success_with_message(data, format!("IP {} added to blacklist", ip))
80}
81
82pub async fn unblock_ip_endpoint(
85 State(_state): State<ApiState>,
86 Path(ip_str): Path<String>,
87) -> ApiResponse<serde_json::Value> {
88 let ip: IpAddr = match ip_str.parse() {
89 Ok(ip) => ip,
90 Err(_) => return ApiResponse::error_typed("invalid_ip", "Invalid IP address format"),
91 };
92
93 let removed = {
94 let Ok(mut blacklist) = IP_BLACKLIST.write() else {
95 return ApiResponse::error_typed("internal_error", "Security subsystem unavailable");
96 };
97 blacklist.remove(&ip)
98 };
99
100 if removed {
101 {
103 if let Ok(mut stats) = SECURITY_STATS.write() {
104 stats.last_updated = Some(chrono::Utc::now().timestamp());
105 }
106 }
107
108 let data = json!({
109 "ip": ip.to_string(),
110 "status": "unblocked"
111 });
112
113 ApiResponse::success_with_message(data, format!("IP {} removed from blacklist", ip))
114 } else {
115 let data = json!({
116 "ip": ip.to_string(),
117 "status": "not_found"
118 });
119
120 ApiResponse::success_with_message(data, format!("IP {} was not in blacklist", ip))
121 }
122}
123
124pub async fn stats_endpoint(State(_state): State<ApiState>) -> ApiResponse<SecurityStatsResponse> {
127 let stats = SECURITY_STATS.read().map(|s| s.clone()).unwrap_or_default();
128 let blacklist_size = IP_BLACKLIST.read().map(|b| b.len()).unwrap_or(0);
129
130 let response_data = SecurityStatsResponse {
131 blocked_requests: stats.blocked_requests,
132 failed_auth_attempts: stats.failed_auth_attempts,
133 suspicious_activity: stats.suspicious_activity,
134 blacklisted_ips: blacklist_size,
135 last_updated: stats.last_updated,
136 };
137
138 ApiResponse::success(response_data)
139}
140
141pub fn is_ip_blacklisted(ip: &IpAddr) -> bool {
143 IP_BLACKLIST.read().map(|b| b.contains(ip)).unwrap_or(false)
144}
145
146pub fn increment_failed_auth() {
148 if let Ok(mut stats) = SECURITY_STATS.write() {
149 stats.failed_auth_attempts += 1;
150 stats.last_updated = Some(chrono::Utc::now().timestamp());
151 }
152}
153
154pub fn increment_suspicious_activity() {
156 if let Ok(mut stats) = SECURITY_STATS.write() {
157 stats.suspicious_activity += 1;
158 stats.last_updated = Some(chrono::Utc::now().timestamp());
159 }
160}