tftsr-devops_investigation/src-tauri/src/commands/docs.rs
Shaun Arman 281e676ad1 fix(security): harden secret handling and audit integrity
Remove high-risk defaults and tighten data handling across auth, storage, IPC, provider calls, and capabilities so sensitive data is better protected by default. Also update README/wiki security guidance and add targeted tests for the new hardening behaviors.

Made-with: Cursor
2026-04-04 23:37:05 -05:00

208 lines
6.2 KiB
Rust

use tauri::State;
use tracing::warn;
use crate::db::models::AuditEntry;
use crate::docs::{exporter, generate_postmortem_markdown, generate_rca_markdown};
use crate::state::AppState;
use serde::{Deserialize, Serialize};
/// Document record returned to the frontend.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Document {
pub id: String,
pub issue_id: String,
pub doc_type: String,
pub title: String,
pub content_md: String,
pub created_at: String,
pub updated_at: String,
}
#[tauri::command]
pub async fn generate_rca(
issue_id: String,
state: State<'_, AppState>,
) -> Result<Document, String> {
let issue_detail = super::db::get_issue(issue_id.clone(), state.clone()).await?;
let content_md = generate_rca_markdown(&issue_detail);
let doc_id = uuid::Uuid::now_v7().to_string();
let now = chrono::Utc::now().format("%Y-%m-%d %H:%M:%S").to_string();
let document = Document {
id: doc_id.clone(),
issue_id: issue_id.clone(),
doc_type: "rca".to_string(),
title: format!("RCA: {title}", title = issue_detail.issue.title),
content_md: content_md.clone(),
created_at: now.clone(),
updated_at: now,
};
// Audit - capture document metadata
let db = state.db.lock().map_err(|e| e.to_string())?;
let audit_details = serde_json::json!({
"issue_id": issue_id,
"issue_title": issue_detail.issue.title,
"doc_type": "rca",
"doc_title": document.title,
"content_length": content_md.len(),
"content_preview": if content_md.len() > 300 {
format!("{preview}...", preview = &content_md[..300])
} else {
content_md.clone()
},
});
let entry = AuditEntry::new(
"generate_rca".to_string(),
"document".to_string(),
doc_id,
audit_details.to_string(),
);
if let Err(err) = crate::audit::log::write_audit_event(
&db,
&entry.action,
&entry.entity_type,
&entry.entity_id,
&entry.details,
) {
warn!(error = %err, "failed to write generate_rca audit entry");
}
Ok(document)
}
#[tauri::command]
pub async fn generate_postmortem(
issue_id: String,
state: State<'_, AppState>,
) -> Result<Document, String> {
let issue_detail = super::db::get_issue(issue_id.clone(), state.clone()).await?;
let content_md = generate_postmortem_markdown(&issue_detail);
let doc_id = uuid::Uuid::now_v7().to_string();
let now = chrono::Utc::now().format("%Y-%m-%d %H:%M:%S").to_string();
let document = Document {
id: doc_id.clone(),
issue_id: issue_id.clone(),
doc_type: "postmortem".to_string(),
title: format!("Post-Mortem: {title}", title = issue_detail.issue.title),
content_md: content_md.clone(),
created_at: now.clone(),
updated_at: now,
};
// Audit - capture document metadata
let db = state.db.lock().map_err(|e| e.to_string())?;
let audit_details = serde_json::json!({
"issue_id": issue_id,
"issue_title": issue_detail.issue.title,
"doc_type": "postmortem",
"doc_title": document.title,
"content_length": content_md.len(),
"content_preview": if content_md.len() > 300 {
format!("{preview}...", preview = &content_md[..300])
} else {
content_md.clone()
},
});
let entry = AuditEntry::new(
"generate_postmortem".to_string(),
"document".to_string(),
doc_id,
audit_details.to_string(),
);
if let Err(err) = crate::audit::log::write_audit_event(
&db,
&entry.action,
&entry.entity_type,
&entry.entity_id,
&entry.details,
) {
warn!(error = %err, "failed to write generate_postmortem audit entry");
}
Ok(document)
}
#[tauri::command]
pub async fn update_document(doc_id: String, content_md: String) -> Result<(), String> {
// Documents are generated on-demand and held in memory / frontend state.
// This is a no-op placeholder. In a future version with a documents table,
// this would persist updates.
if doc_id.is_empty() || content_md.is_empty() {
return Err("doc_id and content_md are required".to_string());
}
Ok(())
}
#[tauri::command]
pub async fn export_document(
title: String,
content_md: String,
format: String,
output_dir: String,
state: State<'_, AppState>,
) -> Result<String, String> {
use std::path::PathBuf;
// Determine the output directory
let base_dir = if output_dir.is_empty() || output_dir == "." {
// Try to use the Downloads directory, fall back to app data dir
dirs::download_dir().unwrap_or_else(|| {
let app_data = state.app_data_dir.clone();
app_data.join("exports")
})
} else {
PathBuf::from(&output_dir)
};
// Ensure the directory exists
std::fs::create_dir_all(&base_dir).map_err(|e| {
format!(
"Failed to create export directory {}: {}",
base_dir.display(),
e
)
})?;
let safe_title: String = title
.chars()
.map(|c| {
if c.is_alphanumeric() || c == '-' || c == '_' {
c
} else {
'_'
}
})
.collect();
let output_path = match format.as_str() {
"markdown" | "md" => {
let path = base_dir.join(format!("{safe_title}.md"));
exporter::export_markdown(&content_md, path.to_str().unwrap())
.map_err(|e| e.to_string())?;
path.to_string_lossy().to_string()
}
"pdf" => {
let path = base_dir.join(format!("{safe_title}.pdf"));
exporter::export_pdf(&content_md, &title, path.to_str().unwrap())
.map_err(|e| e.to_string())?;
path.to_string_lossy().to_string()
}
"docx" => {
let path = base_dir.join(format!("{safe_title}.docx"));
exporter::export_docx(&content_md, &title, path.to_str().unwrap())
.map_err(|e| e.to_string())?;
path.to_string_lossy().to_string()
}
_ => return Err(format!("Unsupported export format: {format}")),
};
Ok(output_path)
}