Skip to main content

mockforge_registry_server/handlers/
security.rs

1//! Security and suspicious activity handlers
2//!
3//! Provides endpoints for detecting and managing suspicious activities
4
5use axum::{
6    extract::{Path, Query, State},
7    http::HeaderMap,
8    Json,
9};
10use serde::{Deserialize, Serialize};
11use uuid::Uuid;
12
13use crate::{
14    error::{ApiError, ApiResult},
15    middleware::{resolve_org_context, AuthUser},
16    AppState,
17};
18
19#[derive(Debug, Serialize)]
20pub struct SuspiciousActivityResponse {
21    pub id: Uuid,
22    pub org_id: Option<Uuid>,
23    pub user_id: Option<Uuid>,
24    pub activity_type: String,
25    pub severity: String,
26    pub description: String,
27    pub metadata: Option<serde_json::Value>,
28    pub ip_address: Option<String>,
29    pub user_agent: Option<String>,
30    pub resolved: bool,
31    pub resolved_at: Option<chrono::DateTime<chrono::Utc>>,
32    pub created_at: chrono::DateTime<chrono::Utc>,
33}
34
35#[derive(Debug, Deserialize)]
36pub struct SuspiciousActivityQuery {
37    pub severity: Option<String>,
38    pub limit: Option<i64>,
39}
40
41#[derive(Debug, Serialize)]
42pub struct SuspiciousActivityListResponse {
43    pub activities: Vec<SuspiciousActivityResponse>,
44    pub total: i64,
45}
46
47/// Get suspicious activities for an organization (admin only)
48pub async fn get_suspicious_activities(
49    State(state): State<AppState>,
50    AuthUser(user_id): AuthUser,
51    headers: HeaderMap,
52    Query(query): Query<SuspiciousActivityQuery>,
53) -> ApiResult<Json<SuspiciousActivityListResponse>> {
54    // Resolve org context
55    let org_ctx = resolve_org_context(&state, user_id, &headers, None)
56        .await
57        .map_err(|_| ApiError::InvalidRequest("Organization not found".to_string()))?;
58
59    // Get activities for this org
60    let activities = state
61        .store
62        .list_unresolved_suspicious_activities(
63            Some(org_ctx.org_id),
64            None,
65            query.severity.as_deref(),
66            query.limit.or(Some(100)),
67        )
68        .await?;
69
70    // Get total count
71    let total = state.store.count_unresolved_suspicious_activities(org_ctx.org_id).await?;
72
73    let activity_responses: Vec<SuspiciousActivityResponse> = activities
74        .into_iter()
75        .map(|a| SuspiciousActivityResponse {
76            id: a.id,
77            org_id: a.org_id,
78            user_id: a.user_id,
79            activity_type: format!("{:?}", a.activity_type),
80            severity: a.severity,
81            description: a.description,
82            metadata: a.metadata,
83            ip_address: a.ip_address,
84            user_agent: a.user_agent,
85            resolved: a.resolved,
86            resolved_at: a.resolved_at,
87            created_at: a.created_at,
88        })
89        .collect();
90
91    Ok(Json(SuspiciousActivityListResponse {
92        activities: activity_responses,
93        total,
94    }))
95}
96
97/// Mark suspicious activity as resolved.
98///
99/// Scoped to the caller's organization so a user from org A cannot resolve
100/// an activity belonging to org B by guessing or leaking its UUID. The
101/// store enforces this constraint inside the UPDATE statement.
102pub async fn resolve_suspicious_activity(
103    State(state): State<AppState>,
104    AuthUser(user_id): AuthUser,
105    headers: HeaderMap,
106    Path(activity_id): Path<Uuid>,
107) -> ApiResult<Json<serde_json::Value>> {
108    let org_ctx = resolve_org_context(&state, user_id, &headers, None)
109        .await
110        .map_err(|_| ApiError::InvalidRequest("Organization not found".to_string()))?;
111
112    state
113        .store
114        .resolve_suspicious_activity(org_ctx.org_id, activity_id, user_id)
115        .await?;
116
117    Ok(Json(serde_json::json!({
118        "success": true,
119        "message": "Suspicious activity marked as resolved"
120    })))
121}