botserver/src/compliance/handlers.rs
Rodrigo Rodriguez 5ea171d126
Some checks failed
BotServer CI / build (push) Failing after 1m34s
Refactor: Split large files into modular subdirectories
Split 20+ files over 1000 lines into focused subdirectories for better
maintainability and code organization. All changes maintain backward
compatibility through re-export wrappers.

Major splits:
- attendance/llm_assist.rs (2074→7 modules)
- basic/keywords/face_api.rs → face_api/ (7 modules)
- basic/keywords/file_operations.rs → file_ops/ (8 modules)
- basic/keywords/hear_talk.rs → hearing/ (6 modules)
- channels/wechat.rs → wechat/ (10 modules)
- channels/youtube.rs → youtube/ (5 modules)
- contacts/mod.rs → contacts_api/ (6 modules)
- core/bootstrap/mod.rs → bootstrap/ (5 modules)
- core/shared/admin.rs → admin_*.rs (5 modules)
- designer/canvas.rs → canvas_api/ (6 modules)
- designer/mod.rs → designer_api/ (6 modules)
- docs/handlers.rs → handlers_api/ (11 modules)
- drive/mod.rs → drive_handlers.rs, drive_types.rs
- learn/mod.rs → types.rs
- main.rs → main_module/ (7 modules)
- meet/webinar.rs → webinar_api/ (8 modules)
- paper/mod.rs → (10 modules)
- security/auth.rs → auth_api/ (7 modules)
- security/passkey.rs → (4 modules)
- sources/mod.rs → sources_api/ (5 modules)
- tasks/mod.rs → task_api/ (5 modules)

Stats: 38,040 deletions, 1,315 additions across 318 files

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2026-02-12 21:09:30 +00:00

617 lines
21 KiB
Rust

use axum::{
extract::{Path, Query, State},
Json,
};
use chrono::Utc;
use diesel::prelude::*;
use std::sync::Arc;
use uuid::Uuid;
use crate::core::bot::get_default_bot;
use crate::core::shared::schema::{
compliance_audit_log, compliance_checks, compliance_issues, compliance_training_records,
};
use crate::core::shared::state::AppState;
use super::storage::{
db_audit_to_entry, db_check_to_result, db_issue_to_result, DbAuditLog, DbComplianceCheck,
DbComplianceIssue, DbTrainingRecord,
};
use super::types::{
AuditLogEntry, ComplianceCheckResult, ComplianceFramework, ComplianceIssueResult,
ComplianceReport, CreateAuditLogRequest, CreateIssueRequest, CreateTrainingRequest,
ListAuditLogsQuery, ListChecksQuery, ListIssuesQuery, RunCheckRequest, TrainingRecord,
UpdateIssueRequest,
};
use super::ComplianceError;
pub async fn handle_list_checks(
State(state): State<Arc<AppState>>,
Query(query): Query<ListChecksQuery>,
) -> Result<Json<Vec<ComplianceCheckResult>>, ComplianceError> {
let pool = state.conn.clone();
let result = tokio::task::spawn_blocking(move || {
let mut conn = pool
.get()
.map_err(|e| ComplianceError::Database(e.to_string()))?;
let (bot_id, _) = get_default_bot(&mut conn);
let limit = query.limit.unwrap_or(50);
let offset = query.offset.unwrap_or(0);
let mut db_query = compliance_checks::table
.filter(compliance_checks::bot_id.eq(bot_id))
.into_boxed();
if let Some(framework) = query.framework {
db_query = db_query.filter(compliance_checks::framework.eq(framework));
}
if let Some(status) = query.status {
db_query = db_query.filter(compliance_checks::status.eq(status));
}
let db_checks: Vec<DbComplianceCheck> = db_query
.order(compliance_checks::checked_at.desc())
.offset(offset)
.limit(limit)
.load(&mut conn)
.map_err(|e| ComplianceError::Database(e.to_string()))?;
let mut results = Vec::new();
for check in db_checks {
let check_id = check.id;
let db_issues: Vec<DbComplianceIssue> = compliance_issues::table
.filter(compliance_issues::check_id.eq(check_id))
.load(&mut conn)
.unwrap_or_default();
let issues: Vec<ComplianceIssueResult> =
db_issues.into_iter().map(db_issue_to_result).collect();
results.push(db_check_to_result(check, issues));
}
Ok::<_, ComplianceError>(results)
})
.await
.map_err(|e| ComplianceError::Internal(e.to_string()))??;
Ok(Json(result))
}
pub async fn handle_run_check(
State(state): State<Arc<AppState>>,
Json(req): Json<RunCheckRequest>,
) -> Result<Json<Vec<ComplianceCheckResult>>, ComplianceError> {
let pool = state.conn.clone();
let result = tokio::task::spawn_blocking(move || {
let mut conn = pool
.get()
.map_err(|e| ComplianceError::Database(e.to_string()))?;
let (bot_id, _) = get_default_bot(&mut conn);
let org_id = Uuid::nil(); // Placeholder - org_id not available from get_default_bot
let now = Utc::now();
let controls = match req.framework {
ComplianceFramework::Gdpr => vec![
("gdpr_7.2", "Data Retention Policy", 95.0),
("gdpr_5.1.f", "Data Protection Measures", 100.0),
("gdpr_6.1", "Lawful Basis for Processing", 98.0),
],
ComplianceFramework::Soc2 => vec![("cc6.1", "Logical and Physical Access Controls", 94.0)],
ComplianceFramework::Iso27001 => vec![("a.8.1", "Inventory of Assets", 90.0)],
ComplianceFramework::Hipaa => vec![("164.312", "Technical Safeguards", 85.0)],
ComplianceFramework::PciDss => vec![("req_3", "Protect Stored Cardholder Data", 88.0)],
};
let mut results = Vec::new();
for (control_id, control_name, score) in controls {
let db_check = DbComplianceCheck {
id: Uuid::new_v4(),
org_id,
bot_id,
framework: req.framework.to_string(),
control_id: control_id.to_string(),
control_name: control_name.to_string(),
status: "compliant".to_string(),
score: bigdecimal::BigDecimal::try_from(score).unwrap_or_default(),
checked_at: now,
checked_by: None,
evidence: serde_json::json!(["Automated check completed"]),
notes: None,
created_at: now,
updated_at: now,
};
diesel::insert_into(compliance_checks::table)
.values(&db_check)
.execute(&mut conn)
.map_err(|e| ComplianceError::Database(e.to_string()))?;
results.push(db_check_to_result(db_check, vec![]));
}
Ok::<_, ComplianceError>(results)
})
.await
.map_err(|e| ComplianceError::Internal(e.to_string()))??;
Ok(Json(result))
}
pub async fn handle_get_check(
State(state): State<Arc<AppState>>,
Path(check_id): Path<Uuid>,
) -> Result<Json<Option<ComplianceCheckResult>>, ComplianceError> {
let pool = state.conn.clone();
let result = tokio::task::spawn_blocking(move || {
let mut conn = pool
.get()
.map_err(|e| ComplianceError::Database(e.to_string()))?;
let db_check: Option<DbComplianceCheck> = compliance_checks::table
.find(check_id)
.first(&mut conn)
.optional()
.map_err(|e| ComplianceError::Database(e.to_string()))?;
match db_check {
Some(check) => {
let db_issues: Vec<DbComplianceIssue> = compliance_issues::table
.filter(compliance_issues::check_id.eq(check_id))
.load(&mut conn)
.unwrap_or_default();
let issues: Vec<ComplianceIssueResult> =
db_issues.into_iter().map(db_issue_to_result).collect();
Ok::<_, ComplianceError>(Some(db_check_to_result(check, issues)))
}
None => Ok(None),
}
})
.await
.map_err(|e| ComplianceError::Internal(e.to_string()))??;
Ok(Json(result))
}
pub async fn handle_list_issues(
State(state): State<Arc<AppState>>,
Query(query): Query<ListIssuesQuery>,
) -> Result<Json<Vec<ComplianceIssueResult>>, ComplianceError> {
let pool = state.conn.clone();
let result = tokio::task::spawn_blocking(move || {
let mut conn = pool
.get()
.map_err(|e| ComplianceError::Database(e.to_string()))?;
let (bot_id, _) = get_default_bot(&mut conn);
let limit = query.limit.unwrap_or(50);
let offset = query.offset.unwrap_or(0);
let mut db_query = compliance_issues::table
.filter(compliance_issues::bot_id.eq(bot_id))
.into_boxed();
if let Some(severity) = query.severity {
db_query = db_query.filter(compliance_issues::severity.eq(severity));
}
if let Some(status) = query.status {
db_query = db_query.filter(compliance_issues::status.eq(status));
}
if let Some(assigned_to) = query.assigned_to {
db_query = db_query.filter(compliance_issues::assigned_to.eq(assigned_to));
}
let db_issues: Vec<DbComplianceIssue> = db_query
.order(compliance_issues::created_at.desc())
.offset(offset)
.limit(limit)
.load(&mut conn)
.map_err(|e| ComplianceError::Database(e.to_string()))?;
let issues: Vec<ComplianceIssueResult> =
db_issues.into_iter().map(db_issue_to_result).collect();
Ok::<_, ComplianceError>(issues)
})
.await
.map_err(|e| ComplianceError::Internal(e.to_string()))??;
Ok(Json(result))
}
pub async fn handle_create_issue(
State(state): State<Arc<AppState>>,
Json(req): Json<CreateIssueRequest>,
) -> Result<Json<ComplianceIssueResult>, ComplianceError> {
let pool = state.conn.clone();
let result = tokio::task::spawn_blocking(move || {
let mut conn = pool
.get()
.map_err(|e| ComplianceError::Database(e.to_string()))?;
let (bot_id, _) = get_default_bot(&mut conn);
let org_id = Uuid::nil(); // Placeholder - org_id not available from get_default_bot
let now = Utc::now();
let db_issue = DbComplianceIssue {
id: Uuid::new_v4(),
org_id,
bot_id,
check_id: req.check_id,
severity: req.severity.to_string(),
title: req.title,
description: req.description,
remediation: req.remediation,
due_date: req.due_date,
assigned_to: req.assigned_to,
status: "open".to_string(),
resolved_at: None,
resolved_by: None,
resolution_notes: None,
created_at: now,
updated_at: now,
};
diesel::insert_into(compliance_issues::table)
.values(&db_issue)
.execute(&mut conn)
.map_err(|e| ComplianceError::Database(e.to_string()))?;
Ok::<_, ComplianceError>(db_issue_to_result(db_issue))
})
.await
.map_err(|e| ComplianceError::Internal(e.to_string()))??;
Ok(Json(result))
}
pub async fn handle_update_issue(
State(state): State<Arc<AppState>>,
Path(issue_id): Path<Uuid>,
Json(req): Json<UpdateIssueRequest>,
) -> Result<Json<ComplianceIssueResult>, ComplianceError> {
let pool = state.conn.clone();
let result = tokio::task::spawn_blocking(move || {
let mut conn = pool
.get()
.map_err(|e| ComplianceError::Database(e.to_string()))?;
let now = Utc::now();
let mut db_issue: DbComplianceIssue = compliance_issues::table
.find(issue_id)
.first(&mut conn)
.map_err(|_| ComplianceError::NotFound("Issue not found".to_string()))?;
if let Some(severity) = req.severity {
db_issue.severity = severity.to_string();
}
if let Some(title) = req.title {
db_issue.title = title;
}
if let Some(description) = req.description {
db_issue.description = description;
}
if let Some(remediation) = req.remediation {
db_issue.remediation = Some(remediation);
}
if let Some(due_date) = req.due_date {
db_issue.due_date = Some(due_date);
}
if let Some(assigned_to) = req.assigned_to {
db_issue.assigned_to = Some(assigned_to);
}
if let Some(status) = req.status {
db_issue.status = status.clone();
if status == "resolved" {
db_issue.resolved_at = Some(now);
}
}
if let Some(resolution_notes) = req.resolution_notes {
db_issue.resolution_notes = Some(resolution_notes);
}
db_issue.updated_at = now;
diesel::update(compliance_issues::table.find(issue_id))
.set(&db_issue)
.execute(&mut conn)
.map_err(|e| ComplianceError::Database(e.to_string()))?;
Ok::<_, ComplianceError>(db_issue_to_result(db_issue))
})
.await
.map_err(|e| ComplianceError::Internal(e.to_string()))??;
Ok(Json(result))
}
pub async fn handle_list_audit_logs(
State(state): State<Arc<AppState>>,
Query(query): Query<ListAuditLogsQuery>,
) -> Result<Json<Vec<AuditLogEntry>>, ComplianceError> {
let pool = state.conn.clone();
let result = tokio::task::spawn_blocking(move || {
let mut conn = pool
.get()
.map_err(|e| ComplianceError::Database(e.to_string()))?;
let (bot_id, _) = get_default_bot(&mut conn);
let limit = query.limit.unwrap_or(100);
let offset = query.offset.unwrap_or(0);
let mut db_query = compliance_audit_log::table
.filter(compliance_audit_log::bot_id.eq(bot_id))
.into_boxed();
if let Some(event_type) = query.event_type {
db_query = db_query.filter(compliance_audit_log::event_type.eq(event_type));
}
if let Some(user_id) = query.user_id {
db_query = db_query.filter(compliance_audit_log::user_id.eq(user_id));
}
if let Some(resource_type) = query.resource_type {
db_query = db_query.filter(compliance_audit_log::resource_type.eq(resource_type));
}
if let Some(from_date) = query.from_date {
db_query = db_query.filter(compliance_audit_log::created_at.ge(from_date));
}
if let Some(to_date) = query.to_date {
db_query = db_query.filter(compliance_audit_log::created_at.le(to_date));
}
let db_logs: Vec<DbAuditLog> = db_query
.order(compliance_audit_log::created_at.desc())
.offset(offset)
.limit(limit)
.load(&mut conn)
.map_err(|e| ComplianceError::Database(e.to_string()))?;
let logs: Vec<AuditLogEntry> = db_logs.into_iter().map(db_audit_to_entry).collect();
Ok::<_, ComplianceError>(logs)
})
.await
.map_err(|e| ComplianceError::Internal(e.to_string()))??;
Ok(Json(result))
}
pub async fn handle_create_audit_log(
State(state): State<Arc<AppState>>,
Json(req): Json<CreateAuditLogRequest>,
) -> Result<Json<AuditLogEntry>, ComplianceError> {
let pool = state.conn.clone();
let result = tokio::task::spawn_blocking(move || {
let mut conn = pool
.get()
.map_err(|e| ComplianceError::Database(e.to_string()))?;
let (bot_id, _) = get_default_bot(&mut conn);
let org_id = Uuid::nil(); // Placeholder - org_id not available from get_default_bot
let now = Utc::now();
let metadata = req.metadata.unwrap_or_default();
let db_log = DbAuditLog {
id: Uuid::new_v4(),
org_id,
bot_id,
event_type: req.event_type.to_string(),
user_id: req.user_id,
resource_type: req.resource_type,
resource_id: req.resource_id,
action: req.action,
result: req.result.to_string(),
ip_address: req.ip_address,
user_agent: req.user_agent,
metadata: serde_json::to_value(&metadata).unwrap_or_default(),
created_at: now,
};
diesel::insert_into(compliance_audit_log::table)
.values(&db_log)
.execute(&mut conn)
.map_err(|e| ComplianceError::Database(e.to_string()))?;
Ok::<_, ComplianceError>(db_audit_to_entry(db_log))
})
.await
.map_err(|e| ComplianceError::Internal(e.to_string()))??;
Ok(Json(result))
}
pub async fn handle_create_training(
State(state): State<Arc<AppState>>,
Json(req): Json<CreateTrainingRequest>,
) -> Result<Json<TrainingRecord>, ComplianceError> {
let pool = state.conn.clone();
let result = tokio::task::spawn_blocking(move || {
let mut conn = pool
.get()
.map_err(|e| ComplianceError::Database(e.to_string()))?;
let (bot_id, _) = get_default_bot(&mut conn);
let org_id = Uuid::nil(); // Placeholder - org_id not available from get_default_bot
let now = Utc::now();
let db_training = DbTrainingRecord {
id: Uuid::new_v4(),
org_id,
bot_id,
user_id: req.user_id,
training_type: req.training_type.to_string(),
training_name: req.training_name.clone(),
provider: req.provider.clone(),
score: req.score,
passed: req.passed,
completion_date: now,
valid_until: req.valid_until,
certificate_url: req.certificate_url.clone(),
metadata: serde_json::json!({}),
created_at: now,
};
diesel::insert_into(compliance_training_records::table)
.values(&db_training)
.execute(&mut conn)
.map_err(|e| ComplianceError::Database(e.to_string()))?;
Ok::<_, ComplianceError>(TrainingRecord {
id: db_training.id,
user_id: db_training.user_id,
training_type: req.training_type,
training_name: req.training_name,
provider: req.provider,
score: req.score,
passed: req.passed,
completion_date: db_training.completion_date,
valid_until: req.valid_until,
certificate_url: req.certificate_url,
})
})
.await
.map_err(|e| ComplianceError::Internal(e.to_string()))??;
Ok(Json(result))
}
pub async fn handle_get_report(
State(state): State<Arc<AppState>>,
Query(query): Query<ListChecksQuery>,
) -> Result<Json<ComplianceReport>, ComplianceError> {
let pool = state.conn.clone();
let result = tokio::task::spawn_blocking(move || {
let mut conn = pool
.get()
.map_err(|e| ComplianceError::Database(e.to_string()))?;
let (bot_id, _) = get_default_bot(&mut conn);
let now = Utc::now();
let mut db_query = compliance_checks::table
.filter(compliance_checks::bot_id.eq(bot_id))
.into_boxed();
if let Some(framework) = query.framework {
db_query = db_query.filter(compliance_checks::framework.eq(framework));
}
let db_checks: Vec<DbComplianceCheck> = db_query
.order(compliance_checks::checked_at.desc())
.limit(100)
.load(&mut conn)
.map_err(|e| ComplianceError::Database(e.to_string()))?;
let mut results = Vec::new();
let mut total_score = 0.0;
let mut compliant_count = 0;
for check in db_checks {
let check_id = check.id;
let score: f64 = check.score.to_string().parse().unwrap_or(0.0);
total_score += score;
if check.status == "compliant" {
compliant_count += 1;
}
let db_issues: Vec<DbComplianceIssue> = compliance_issues::table
.filter(compliance_issues::check_id.eq(check_id))
.load(&mut conn)
.unwrap_or_default();
let issues: Vec<ComplianceIssueResult> =
db_issues.into_iter().map(db_issue_to_result).collect();
results.push(db_check_to_result(check, issues));
}
let total_controls = results.len();
let overall_score = if total_controls > 0 {
total_score / total_controls as f64
} else {
0.0
};
let all_issues: Vec<DbComplianceIssue> = compliance_issues::table
.filter(compliance_issues::bot_id.eq(bot_id))
.filter(compliance_issues::status.ne("resolved"))
.load(&mut conn)
.unwrap_or_default();
let mut critical = 0;
let mut high = 0;
let mut medium = 0;
let mut low = 0;
for issue in &all_issues {
match issue.severity.as_str() {
"critical" => critical += 1,
"high" => high += 1,
"medium" => medium += 1,
"low" => low += 1,
_ => {}
}
}
Ok::<_, ComplianceError>(ComplianceReport {
generated_at: now,
overall_score,
total_controls_checked: total_controls,
compliant_controls: compliant_count,
total_issues: all_issues.len(),
critical_issues: critical,
high_issues: high,
medium_issues: medium,
low_issues: low,
results,
})
})
.await
.map_err(|e| ComplianceError::Internal(e.to_string()))??;
Ok(Json(result))
}
pub async fn handle_upload_evidence(
State(_state): State<Arc<AppState>>,
mut multipart: axum::extract::Multipart,
) -> Result<Json<serde_json::Value>, ComplianceError> {
let mut file_name = String::new();
let mut category = String::new();
let mut file_size = 0usize;
while let Some(field) = multipart.next_field().await.map_err(|e| ComplianceError::Internal(e.to_string()))? {
let name = field.name().unwrap_or("").to_string();
match name.as_str() {
"file" => {
file_name = field.file_name().unwrap_or("unknown").to_string();
let data = field.bytes().await.map_err(|e| ComplianceError::Internal(e.to_string()))?;
file_size = data.len();
}
"category" => {
category = field.text().await.map_err(|e| ComplianceError::Internal(e.to_string()))?;
}
_ => {}
}
}
let evidence_id = Uuid::new_v4();
Ok(Json(serde_json::json!({
"success": true,
"evidence_id": evidence_id,
"file_name": file_name,
"category": category,
"file_size": file_size,
"uploaded_at": Utc::now().to_rfc3339()
})))
}