Skip to main content

mkt_ksa_geo_sec/api/
mod.rs

1/******************************************************************************************
2      📍 منصة تحليل الأمان الجغرافي MKT KSA – تطوير منصور بن خالد
3* 📄 رخصة Apache 2.0 – يسمح بالاستخدام والتعديل بشرط النسبة وعدم تقديم ضمانات.
4* MKT KSA Geolocation Security – Developed by Mansour Bin Khalid (KSA 🇸🇦)
5* Licensed under Apache 2.0 – https://www.apache.org/licenses/LICENSE-2.0
6* © 2025 All rights reserved.
7
8     File Name: mod.rs
9    Path:      src/api/mod.rs
10
11
12    File Role:
13    هذا الملف هو "موجه المرور" لطبقة الـ API. يقوم بتجميع وتسجيل جميع
14    نقاط النهاية (Endpoints) من الوحدات المختلفة (مثل auth, geo, device)
15    في مكان واحد، لتقديمها إلى خادم `actix-web` الرئيسي.
16
17    Main Tasks:
18    1.  الإعلان عن جميع وحدات API الفرعية.
19    2.  توفير دالة `config` واحدة لتسجيل جميع خدمات API.
20    --------------------------------------------------------------
21    File Name: mod.rs
22    Path:      src/api/mod.rs
23
24    File Role:
25    This file is the "traffic director" for the API layer. It aggregates and
26    registers all endpoints from the different modules (like auth, geo, device)
27    in a single place to be served by the main `actix-web` server.
28
29    Main Tasks:
30    1.  Declare all API sub-modules.
31    2.  Provide a single `config` function to register all API services.
32******************************************************************************************/
33
34use actix_web::http::StatusCode;
35use actix_web::web;
36use actix_web::HttpResponse;
37use actix_web::{dev::Payload, FromRequest, HttpRequest};
38use serde::de::DeserializeOwned;
39use serde::Serialize;
40use subtle::ConstantTimeEq;
41use uuid::Uuid;
42use zeroize::Zeroize;
43
44use crate::security::jwt::Claims;
45use crate::security::ratelimit::RateLimitError;
46use crate::AppState;
47use std::future::{ready, Ready};
48use std::net::IpAddr;
49
50// --- وحدات API الفرعية ---
51// --- API Sub-modules ---
52pub mod alerts;
53pub mod auth;
54pub mod behavior;
55pub mod device;
56pub mod geo;
57pub mod network;
58pub mod sensors;
59pub mod smart_access;
60pub mod weather;
61
62#[derive(Serialize)]
63struct ApiErrorBody {
64    code: &'static str,
65    message: &'static str,
66    request_id: Option<String>,
67}
68
69pub fn api_error(status: StatusCode, code: &'static str, message: &'static str) -> HttpResponse {
70    HttpResponse::build(status).json(ApiErrorBody {
71        code,
72        message,
73        request_id: None,
74    })
75}
76
77pub fn api_error_with_request_id(
78    status: StatusCode,
79    code: &'static str,
80    message: &'static str,
81    request_id: &str,
82) -> HttpResponse {
83    HttpResponse::build(status)
84        .insert_header(("X-Request-ID", request_id.to_string()))
85        .json(ApiErrorBody {
86            code,
87            message,
88            request_id: Some(request_id.to_string()),
89        })
90}
91
92pub fn parse_json_payload<T: DeserializeOwned>(
93    payload_bytes: &web::Bytes,
94) -> Result<T, HttpResponse> {
95    serde_json::from_slice(payload_bytes).map_err(|_| {
96        api_error(
97            StatusCode::BAD_REQUEST,
98            "INVALID_JSON_PAYLOAD",
99            "Malformed JSON payload",
100        )
101    })
102}
103
104pub fn ok_json_with_trace<T: Serialize>(req: &HttpRequest, data: T) -> HttpResponse {
105    HttpResponse::Ok().json(serde_json::json!({
106        "trace_id": request_id(req),
107        "data": data,
108    }))
109}
110
111pub fn client_ip(req: &HttpRequest) -> IpAddr {
112    req.headers()
113        .get("X-Forwarded-For")
114        .and_then(|hv| hv.to_str().ok())
115        .and_then(|v| v.split(',').next())
116        .and_then(|s| s.trim().parse::<IpAddr>().ok())
117        .or_else(|| req.peer_addr().map(|a| a.ip()))
118        .unwrap_or(IpAddr::from([0, 0, 0, 0]))
119}
120
121pub fn request_id(req: &HttpRequest) -> String {
122    req.headers()
123        .get("X-Request-ID")
124        .and_then(|hv| hv.to_str().ok())
125        .map(str::trim)
126        .filter(|v| !v.is_empty() && v.len() <= 128)
127        .map(ToString::to_string)
128        .unwrap_or_else(|| Uuid::new_v4().to_string())
129}
130
131fn log_security_event(req_id: &str, ip: IpAddr, path: &str, code: &str, detail: &str) {
132    eprintln!(
133        "security_event code={} request_id={} ip={} path={} detail={}",
134        code, req_id, ip, path, detail
135    );
136}
137
138/// Extractor موحّد للحصول على Bearer token من هيدر Authorization
139/// Unified extractor to fetch Bearer token from Authorization header
140pub struct BearerToken(pub String);
141
142impl FromRequest for BearerToken {
143    type Error = actix_web::Error;
144    type Future = Ready<Result<Self, Self::Error>>;
145
146    fn from_request(req: &HttpRequest, _payload: &mut Payload) -> Self::Future {
147        let token = req
148            .headers()
149            .get("Authorization")
150            .and_then(|hv| hv.to_str().ok())
151            .and_then(|s| s.strip_prefix("Bearer "))
152            .map(str::trim)
153            .filter(|s| !s.is_empty())
154            .map(ToString::to_string)
155            .unwrap_or_default();
156        ready(Ok(Self(token)))
157    }
158}
159
160pub async fn authorize_request(
161    app_state: &web::Data<AppState>,
162    req: &HttpRequest,
163    bearer: &BearerToken,
164    payload_bytes: &web::Bytes,
165) -> Result<Claims, HttpResponse> {
166    let ip = client_ip(req);
167    let req_id = request_id(req);
168    let path = req.path();
169
170    match app_state.rate_limiter.check(ip).await {
171        Ok(()) => {}
172        Err(RateLimitError::Blacklisted) => {
173            log_security_event(
174                &req_id,
175                ip,
176                path,
177                "IP_BLACKLISTED",
178                "request rejected by blacklist",
179            );
180            return Err(api_error_with_request_id(
181                StatusCode::FORBIDDEN,
182                "IP_BLACKLISTED",
183                "Client IP is blocked",
184                &req_id,
185            ));
186        }
187        Err(RateLimitError::LimitExceeded) => {
188            let retry_after = app_state.rate_limiter.retry_after_seconds(ip).await;
189            log_security_event(
190                &req_id,
191                ip,
192                path,
193                "RATE_LIMIT_EXCEEDED",
194                "request rejected by rate limit",
195            );
196            return Err(HttpResponse::build(StatusCode::TOO_MANY_REQUESTS)
197                .insert_header(("Retry-After", retry_after.to_string()))
198                .insert_header(("X-Request-ID", req_id.clone()))
199                .json(ApiErrorBody {
200                    code: "RATE_LIMIT_EXCEEDED",
201                    message: "Too many requests",
202                    request_id: Some(req_id.clone()),
203                }));
204        }
205    }
206
207    if let Some(expected_api_key) = &app_state.api_key {
208        let provided = req
209            .headers()
210            .get("X-API-Key")
211            .and_then(|hv| hv.to_str().ok())
212            .map(str::trim)
213            .unwrap_or_default();
214
215        if provided.is_empty() {
216            log_security_event(
217                &req_id,
218                ip,
219                path,
220                "MISSING_API_KEY",
221                "request missing X-API-Key header",
222            );
223            return Err(api_error_with_request_id(
224                StatusCode::UNAUTHORIZED,
225                "MISSING_API_KEY",
226                "Missing API key",
227                &req_id,
228            ));
229        }
230
231        if provided
232            .as_bytes()
233            .ct_eq(expected_api_key.expose().as_bytes())
234            .unwrap_u8()
235            == 0
236        {
237            log_security_event(
238                &req_id,
239                ip,
240                path,
241                "INVALID_API_KEY",
242                "X-API-Key validation failed",
243            );
244            return Err(api_error_with_request_id(
245                StatusCode::UNAUTHORIZED,
246                "INVALID_API_KEY",
247                "Invalid API key",
248                &req_id,
249            ));
250        }
251    }
252
253    let mut token = bearer.0.clone();
254    if token.is_empty() {
255        log_security_event(
256            &req_id,
257            ip,
258            path,
259            "MISSING_BEARER_TOKEN",
260            "missing Authorization bearer token",
261        );
262        return Err(api_error_with_request_id(
263            StatusCode::UNAUTHORIZED,
264            "MISSING_BEARER_TOKEN",
265            "Missing Authorization token",
266            &req_id,
267        ));
268    }
269
270    let claims = app_state.jwt_manager.decode_token(&token).map_err(|_| {
271        log_security_event(
272            &req_id,
273            ip,
274            path,
275            "INVALID_OR_EXPIRED_TOKEN",
276            "JWT token validation failed",
277        );
278        api_error_with_request_id(
279            StatusCode::UNAUTHORIZED,
280            "INVALID_OR_EXPIRED_TOKEN",
281            "Invalid or expired token",
282            &req_id,
283        )
284    });
285    token.zeroize();
286    let claims = claims?;
287
288    let ua = req
289        .headers()
290        .get("User-Agent")
291        .and_then(|h| h.to_str().ok());
292    let ai_decision = app_state
293        .ai_guard
294        .evaluate_request(ip, req.path(), ua, payload_bytes)
295        .await;
296    if ai_decision.blocked {
297        let detail = format!(
298            "ai_block score={} reasons={}",
299            ai_decision.assessment.score,
300            ai_decision.assessment.reasons.join("|")
301        );
302        log_security_event(&req_id, ip, path, "AI_RISK_BLOCKED", &detail);
303        let mut builder = HttpResponse::build(StatusCode::FORBIDDEN);
304        builder.insert_header(("X-Request-ID", req_id.clone()));
305        if let Some(retry_after) = ai_decision.retry_after_seconds {
306            builder.insert_header(("Retry-After", retry_after.to_string()));
307        }
308        return Err(builder.json(serde_json::json!({
309            "code": "AI_RISK_BLOCKED",
310            "message": "Request blocked by adaptive AI security policy",
311            "request_id": req_id,
312            "risk_score": ai_decision.assessment.score,
313            "reasons": ai_decision.assessment.reasons,
314            "retry_after_seconds": ai_decision.retry_after_seconds,
315        })));
316    }
317
318    Ok(claims)
319}
320
321/// Arabic: تقوم هذه الدالة بتسجيل جميع مسارات API في التطبيق.
322/// English: This function registers all API routes in the application.
323pub fn config(cfg: &mut web::ServiceConfig) {
324    cfg.service(
325        web::scope("/api")
326            .service(auth::get_user)
327            .service(geo::resolve_geo)
328            .service(device::resolve_device)
329            .service(behavior::analyze_behavior)
330            .service(sensors::analyze_sensors)
331            .service(network::analyze_network)
332            .service(alerts::trigger_alert)
333            .service(weather::weather_summary)
334            .service(smart_access::smart_access_verify),
335    );
336}