Some checks failed
BotServer CI / build (push) Failing after 1m34s
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>
617 lines
21 KiB
Rust
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()
|
|
})))
|
|
}
|