botserver/src/compliance/ui.rs
Rodrigo Rodriguez (Pragmatismo) 31777432b4 Implement TODO items: session auth, face API, task logs, intent storage
Learn Module:
- All 9 handlers now use AuthenticatedUser extractor

Security:
- validate_session_sync reads roles from SESSION_CACHE

AutoTask:
- get_task_logs reads from manifest with status logs
- store_compiled_intent saves to cache and database

Face API:
- AWS Rekognition, OpenCV, InsightFace implementations
- Detection, verification, analysis methods

Other fixes:
- Calendar/task integration database queries
- Recording database methods
- Analytics insights trends
- Email/folder monitoring mock data
2026-01-13 14:48:49 -03:00

535 lines
27 KiB
Rust

use axum::{
extract::{Path, State},
response::Html,
routing::get,
Router,
};
use std::sync::Arc;
use uuid::Uuid;
use crate::shared::state::AppState;
pub async fn handle_compliance_dashboard_page(State(_state): State<Arc<AppState>>) -> Html<String> {
let html = r#"<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Compliance Dashboard</title>
<style>
* { box-sizing: border-box; margin: 0; padding: 0; }
body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; background: #f5f5f5; }
.container { max-width: 1400px; margin: 0 auto; padding: 24px; }
.header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 24px; }
.header h1 { font-size: 28px; color: #1a1a1a; }
.btn { padding: 10px 20px; border: none; border-radius: 8px; cursor: pointer; font-size: 14px; font-weight: 500; }
.btn-primary { background: #0066cc; color: white; }
.btn-primary:hover { background: #0052a3; }
.stats-row { display: grid; grid-template-columns: repeat(5, 1fr); gap: 16px; margin-bottom: 24px; }
.stat-card { background: white; border-radius: 12px; padding: 20px; box-shadow: 0 2px 8px rgba(0,0,0,0.08); }
.stat-value { font-size: 32px; font-weight: 600; }
.stat-value.green { color: #2e7d32; }
.stat-value.yellow { color: #f9a825; }
.stat-value.red { color: #c62828; }
.stat-label { font-size: 13px; color: #666; margin-top: 4px; }
.stat-change { font-size: 12px; margin-top: 8px; }
.stat-change.positive { color: #2e7d32; }
.stat-change.negative { color: #c62828; }
.tabs { display: flex; gap: 4px; margin-bottom: 24px; border-bottom: 1px solid #e0e0e0; }
.tab { padding: 12px 24px; background: none; border: none; cursor: pointer; font-size: 14px; color: #666; border-bottom: 2px solid transparent; }
.tab.active { color: #0066cc; border-bottom-color: #0066cc; }
.content-grid { display: grid; grid-template-columns: 2fr 1fr; gap: 24px; }
.section { background: white; border-radius: 12px; padding: 24px; box-shadow: 0 2px 8px rgba(0,0,0,0.08); margin-bottom: 24px; }
.section-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 16px; }
.section-title { font-size: 18px; font-weight: 600; }
.framework-card { display: flex; align-items: center; justify-content: space-between; padding: 16px; border: 1px solid #e0e0e0; border-radius: 8px; margin-bottom: 12px; cursor: pointer; }
.framework-card:hover { background: #f8f9fa; }
.framework-info { display: flex; align-items: center; gap: 16px; }
.framework-icon { width: 48px; height: 48px; border-radius: 8px; display: flex; align-items: center; justify-content: center; font-size: 20px; font-weight: 600; }
.framework-gdpr { background: #e3f2fd; color: #1565c0; }
.framework-soc2 { background: #f3e5f5; color: #7b1fa2; }
.framework-iso { background: #e8f5e9; color: #2e7d32; }
.framework-hipaa { background: #fff3e0; color: #ef6c00; }
.framework-pci { background: #fce4ec; color: #c2185b; }
.framework-name { font-weight: 600; font-size: 16px; }
.framework-meta { font-size: 13px; color: #666; }
.score-badge { padding: 8px 16px; border-radius: 20px; font-weight: 600; font-size: 14px; }
.score-high { background: #e8f5e9; color: #2e7d32; }
.score-medium { background: #fff3e0; color: #ef6c00; }
.score-low { background: #ffebee; color: #c62828; }
.issue-item { display: flex; align-items: flex-start; gap: 12px; padding: 12px 0; border-bottom: 1px solid #f0f0f0; }
.issue-item:last-child { border-bottom: none; }
.issue-severity { width: 8px; height: 8px; border-radius: 50%; margin-top: 6px; flex-shrink: 0; }
.severity-critical { background: #c62828; }
.severity-high { background: #ef6c00; }
.severity-medium { background: #f9a825; }
.severity-low { background: #66bb6a; }
.issue-content { flex: 1; }
.issue-title { font-weight: 500; font-size: 14px; margin-bottom: 4px; }
.issue-meta { font-size: 12px; color: #666; }
.progress-ring { width: 120px; height: 120px; }
.empty-state { text-align: center; padding: 40px; color: #666; }
</style>
</head>
<body>
<div class="container">
<div class="header">
<h1>Compliance Dashboard</h1>
<button class="btn btn-primary" onclick="runAudit()">Run Compliance Check</button>
</div>
<div class="stats-row">
<div class="stat-card">
<div class="stat-value green" id="overallScore">--</div>
<div class="stat-label">Overall Score</div>
<div class="stat-change positive">+2.5% from last month</div>
</div>
<div class="stat-card">
<div class="stat-value" id="totalControls">0</div>
<div class="stat-label">Controls Checked</div>
</div>
<div class="stat-card">
<div class="stat-value green" id="compliantControls">0</div>
<div class="stat-label">Compliant</div>
</div>
<div class="stat-card">
<div class="stat-value yellow" id="partialControls">0</div>
<div class="stat-label">Partial</div>
</div>
<div class="stat-card">
<div class="stat-value red" id="openIssues">0</div>
<div class="stat-label">Open Issues</div>
</div>
</div>
<div class="tabs">
<button class="tab active" data-view="overview">Overview</button>
<button class="tab" data-view="frameworks">Frameworks</button>
<button class="tab" data-view="issues">Issues</button>
<button class="tab" data-view="audit-log">Audit Log</button>
<button class="tab" data-view="training">Training</button>
</div>
<div class="content-grid">
<div>
<div class="section">
<div class="section-header">
<h2 class="section-title">Compliance Frameworks</h2>
<button class="btn" style="padding: 6px 12px; font-size: 12px;" onclick="addFramework()">+ Add Framework</button>
</div>
<div id="frameworksList">
<div class="framework-card" onclick="openFramework('gdpr')">
<div class="framework-info">
<div class="framework-icon framework-gdpr">GDPR</div>
<div>
<div class="framework-name">GDPR</div>
<div class="framework-meta">General Data Protection Regulation • 12 controls</div>
</div>
</div>
<span class="score-badge score-high">95%</span>
</div>
<div class="framework-card" onclick="openFramework('soc2')">
<div class="framework-info">
<div class="framework-icon framework-soc2">SOC2</div>
<div>
<div class="framework-name">SOC 2 Type II</div>
<div class="framework-meta">Service Organization Control • 24 controls</div>
</div>
</div>
<span class="score-badge score-high">92%</span>
</div>
<div class="framework-card" onclick="openFramework('iso27001')">
<div class="framework-info">
<div class="framework-icon framework-iso">ISO</div>
<div>
<div class="framework-name">ISO 27001</div>
<div class="framework-meta">Information Security Management • 18 controls</div>
</div>
</div>
<span class="score-badge score-medium">78%</span>
</div>
</div>
</div>
<div class="section">
<div class="section-header">
<h2 class="section-title">Recent Audit Activity</h2>
<a href="/suite/compliance/audit-log" style="color: #0066cc; font-size: 14px; text-decoration: none;">View All →</a>
</div>
<div id="auditActivity">
<div class="empty-state">No recent audit activity</div>
</div>
</div>
</div>
<div>
<div class="section">
<div class="section-header">
<h2 class="section-title">Open Issues</h2>
<a href="/suite/compliance/issues" style="color: #0066cc; font-size: 14px; text-decoration: none;">View All →</a>
</div>
<div id="issuesList">
<div class="issue-item">
<div class="issue-severity severity-critical"></div>
<div class="issue-content">
<div class="issue-title">Data retention policy needs update</div>
<div class="issue-meta">GDPR • Due in 5 days</div>
</div>
</div>
<div class="issue-item">
<div class="issue-severity severity-high"></div>
<div class="issue-content">
<div class="issue-title">Access review overdue for 3 users</div>
<div class="issue-meta">SOC 2 • Due in 2 days</div>
</div>
</div>
<div class="issue-item">
<div class="issue-severity severity-medium"></div>
<div class="issue-content">
<div class="issue-title">Security training incomplete</div>
<div class="issue-meta">ISO 27001 • Due in 14 days</div>
</div>
</div>
</div>
</div>
<div class="section">
<div class="section-header">
<h2 class="section-title">Upcoming Reviews</h2>
</div>
<div id="upcomingReviews">
<div class="issue-item">
<div class="issue-content">
<div class="issue-title">Quarterly Access Review</div>
<div class="issue-meta">Jan 31, 2025</div>
</div>
</div>
<div class="issue-item">
<div class="issue-content">
<div class="issue-title">Annual Security Assessment</div>
<div class="issue-meta">Feb 15, 2025</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<script>
document.querySelectorAll('.tab').forEach(tab => {
tab.addEventListener('click', () => {
document.querySelectorAll('.tab').forEach(t => t.classList.remove('active'));
tab.classList.add('active');
loadView(tab.dataset.view);
});
});
async function loadDashboard() {
try {
const response = await fetch('/api/compliance/report');
const report = await response.json();
if (report) {
document.getElementById('overallScore').textContent = Math.round(report.overall_score || 0) + '%';
document.getElementById('totalControls').textContent = report.total_controls_checked || 0;
document.getElementById('compliantControls').textContent = report.compliant_controls || 0;
document.getElementById('openIssues').textContent = report.total_issues || 0;
}
} catch (e) {
console.error('Failed to load dashboard:', e);
}
}
function loadView(view) {
switch(view) {
case 'issues':
window.location = '/suite/compliance/issues';
break;
case 'audit-log':
window.location = '/suite/compliance/audit-log';
break;
case 'training':
window.location = '/suite/compliance/training';
break;
}
}
function runAudit() {
if (confirm('Run a full compliance check? This may take a few minutes.')) {
fetch('/api/compliance/checks', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ framework: 'gdpr' })
}).then(() => {
alert('Compliance check started');
loadDashboard();
});
}
}
function openFramework(framework) {
window.location = `/suite/compliance/framework/${framework}`;
}
function addFramework() {
alert('Framework configuration coming soon');
}
loadDashboard();
</script>
</body>
</html>"#;
Html(html.to_string())
}
pub async fn handle_compliance_issues_page(State(_state): State<Arc<AppState>>) -> Html<String> {
let html = r#"<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Compliance Issues</title>
<style>
* { box-sizing: border-box; margin: 0; padding: 0; }
body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; background: #f5f5f5; }
.container { max-width: 1200px; margin: 0 auto; padding: 24px; }
.back-link { color: #0066cc; text-decoration: none; display: inline-block; margin-bottom: 16px; }
.header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 24px; }
.header h1 { font-size: 24px; }
.btn { padding: 10px 20px; border: none; border-radius: 8px; cursor: pointer; font-size: 14px; font-weight: 500; }
.btn-primary { background: #0066cc; color: white; }
.filters { display: flex; gap: 12px; margin-bottom: 24px; }
.search-box { flex: 1; padding: 10px 16px; border: 1px solid #ddd; border-radius: 8px; }
.filter-select { padding: 8px 16px; border: 1px solid #ddd; border-radius: 8px; background: white; }
.issues-table { background: white; border-radius: 12px; box-shadow: 0 2px 8px rgba(0,0,0,0.08); overflow: hidden; }
.table-header { display: grid; grid-template-columns: 40px 1fr 120px 120px 120px 100px; padding: 16px 20px; background: #f9f9f9; font-weight: 600; font-size: 13px; color: #666; border-bottom: 1px solid #e0e0e0; }
.table-row { display: grid; grid-template-columns: 40px 1fr 120px 120px 120px 100px; padding: 16px 20px; border-bottom: 1px solid #f0f0f0; align-items: center; cursor: pointer; }
.table-row:hover { background: #f8f9fa; }
.severity-dot { width: 10px; height: 10px; border-radius: 50%; }
.severity-critical { background: #c62828; }
.severity-high { background: #ef6c00; }
.severity-medium { background: #f9a825; }
.severity-low { background: #66bb6a; }
.issue-title { font-weight: 500; }
.issue-framework { font-size: 12px; color: #666; margin-top: 4px; }
.status-badge { padding: 4px 10px; border-radius: 12px; font-size: 12px; font-weight: 500; }
.status-open { background: #ffebee; color: #c62828; }
.status-in-progress { background: #fff3e0; color: #ef6c00; }
.status-resolved { background: #e8f5e9; color: #2e7d32; }
.empty-state { text-align: center; padding: 60px; color: #666; }
</style>
</head>
<body>
<div class="container">
<a href="/suite/compliance" class="back-link">← Back to Compliance</a>
<div class="header">
<h1>Compliance Issues</h1>
<button class="btn btn-primary" onclick="createIssue()">+ Report Issue</button>
</div>
<div class="filters">
<input type="text" class="search-box" placeholder="Search issues..." id="searchInput">
<select class="filter-select" id="severityFilter">
<option value="">All Severities</option>
<option value="critical">Critical</option>
<option value="high">High</option>
<option value="medium">Medium</option>
<option value="low">Low</option>
</select>
<select class="filter-select" id="statusFilter">
<option value="">All Status</option>
<option value="open">Open</option>
<option value="in_progress">In Progress</option>
<option value="resolved">Resolved</option>
</select>
<select class="filter-select" id="frameworkFilter">
<option value="">All Frameworks</option>
<option value="gdpr">GDPR</option>
<option value="soc2">SOC 2</option>
<option value="iso27001">ISO 27001</option>
<option value="hipaa">HIPAA</option>
</select>
</div>
<div class="issues-table">
<div class="table-header">
<span></span>
<span>Issue</span>
<span>Framework</span>
<span>Status</span>
<span>Due Date</span>
<span>Assignee</span>
</div>
<div id="issuesList">
<div class="empty-state">Loading issues...</div>
</div>
</div>
</div>
<script>
async function loadIssues() {
try {
const response = await fetch('/api/compliance/issues');
const issues = await response.json();
renderIssues(issues);
} catch (e) {
console.error('Failed to load issues:', e);
document.getElementById('issuesList').innerHTML = '<div class="empty-state">Failed to load issues</div>';
}
}
function renderIssues(issues) {
const list = document.getElementById('issuesList');
if (!issues || issues.length === 0) {
list.innerHTML = '<div class="empty-state">No compliance issues found</div>';
return;
}
list.innerHTML = issues.map(i => `
<div class="table-row" onclick="openIssue('${i.id}')">
<div class="severity-dot severity-${i.severity || 'medium'}"></div>
<div>
<div class="issue-title">${i.title}</div>
<div class="issue-framework">${i.description ? i.description.substring(0, 60) + '...' : ''}</div>
</div>
<span>${i.framework || '-'}</span>
<span class="status-badge status-${(i.status || 'open').replace(' ', '-')}">${i.status || 'Open'}</span>
<span>${i.due_date ? new Date(i.due_date).toLocaleDateString() : '-'}</span>
<span>${i.assigned_to || 'Unassigned'}</span>
</div>
`).join('');
}
function openIssue(id) {
window.location = `/suite/compliance/issues/${id}`;
}
function createIssue() {
window.location = '/suite/compliance/issues/new';
}
loadIssues();
</script>
</body>
</html>"#;
Html(html.to_string())
}
pub async fn handle_compliance_issue_detail_page(
State(_state): State<Arc<AppState>>,
Path(issue_id): Path<Uuid>,
) -> Html<String> {
let html = format!(r#"<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Compliance Issue</title>
<style>
* {{ box-sizing: border-box; margin: 0; padding: 0; }}
body {{ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; background: #f5f5f5; }}
.container {{ max-width: 900px; margin: 0 auto; padding: 24px; }}
.back-link {{ color: #0066cc; text-decoration: none; display: inline-block; margin-bottom: 16px; }}
.issue-card {{ background: white; border-radius: 12px; padding: 32px; box-shadow: 0 2px 8px rgba(0,0,0,0.08); margin-bottom: 24px; }}
.issue-header {{ display: flex; justify-content: space-between; align-items: flex-start; margin-bottom: 20px; }}
.issue-title {{ font-size: 24px; font-weight: 600; margin-bottom: 12px; }}
.issue-meta {{ display: flex; gap: 16px; flex-wrap: wrap; }}
.meta-item {{ font-size: 13px; color: #666; }}
.severity-badge {{ padding: 4px 12px; border-radius: 12px; font-size: 12px; font-weight: 500; }}
.severity-critical {{ background: #ffebee; color: #c62828; }}
.severity-high {{ background: #fff3e0; color: #ef6c00; }}
.severity-medium {{ background: #fff8e1; color: #f9a825; }}
.severity-low {{ background: #e8f5e9; color: #2e7d32; }}
.issue-description {{ line-height: 1.7; color: #444; margin-bottom: 20px; }}
.section {{ margin-top: 24px; padding-top: 24px; border-top: 1px solid #e0e0e0; }}
.section-title {{ font-size: 16px; font-weight: 600; margin-bottom: 12px; }}
.btn {{ padding: 10px 20px; border: none; border-radius: 8px; cursor: pointer; font-size: 14px; font-weight: 500; }}
.btn-primary {{ background: #0066cc; color: white; }}
.btn-success {{ background: #2e7d32; color: white; }}
.btn-outline {{ background: white; border: 1px solid #ddd; color: #333; }}
.actions {{ display: flex; gap: 12px; }}
.remediation-box {{ background: #f9f9f9; border-radius: 8px; padding: 16px; line-height: 1.6; }}
</style>
</head>
<body>
<div class="container">
<a href="/suite/compliance/issues" class="back-link">← Back to Issues</a>
<div class="issue-card">
<div class="issue-header">
<div>
<h1 class="issue-title" id="issueTitle">Loading...</h1>
<div class="issue-meta">
<span class="severity-badge severity-medium" id="issueSeverity">Medium</span>
<span class="meta-item" id="issueFramework">Framework: -</span>
<span class="meta-item" id="issueStatus">Status: Open</span>
<span class="meta-item" id="issueDue">Due: -</span>
</div>
</div>
<div class="actions">
<button class="btn btn-success" onclick="resolveIssue()">Mark Resolved</button>
<button class="btn btn-outline" onclick="editIssue()">Edit</button>
</div>
</div>
<div class="issue-description" id="issueDescription">
Loading issue details...
</div>
<div class="section">
<h3 class="section-title">Remediation Steps</h3>
<div class="remediation-box" id="issueRemediation">
No remediation steps provided.
</div>
</div>
<div class="section">
<h3 class="section-title">Assignment</h3>
<p id="issueAssignee">Unassigned</p>
</div>
</div>
</div>
<script>
const issueId = '{issue_id}';
async function loadIssue() {{
try {{
const response = await fetch(`/api/compliance/issues`);
const issues = await response.json();
const issue = issues.find(i => i.id === issueId);
if (issue) {{
document.getElementById('issueTitle').textContent = issue.title;
document.getElementById('issueDescription').textContent = issue.description || 'No description provided.';
document.getElementById('issueRemediation').textContent = issue.remediation || 'No remediation steps provided.';
const severityEl = document.getElementById('issueSeverity');
severityEl.textContent = (issue.severity || 'medium').charAt(0).toUpperCase() + (issue.severity || 'medium').slice(1);
severityEl.className = `severity-badge severity-${{issue.severity || 'medium'}}`;
document.getElementById('issueFramework').textContent = `Framework: ${{issue.framework || '-'}}`;
document.getElementById('issueStatus').textContent = `Status: ${{issue.status || 'Open'}}`;
document.getElementById('issueDue').textContent = issue.due_date ? `Due: ${{new Date(issue.due_date).toLocaleDateString()}}` : 'Due: -';
document.getElementById('issueAssignee').textContent = issue.assigned_to || 'Unassigned';
}}
}} catch (e) {{
console.error('Failed to load issue:', e);
}}
}}
async function resolveIssue() {{
if (!confirm('Mark this issue as resolved?')) return;
try {{
await fetch(`/api/compliance/issues/${{issueId}}`, {{
method: 'PUT',
headers: {{ 'Content-Type': 'application/json' }},
body: JSON.stringify({{ status: 'resolved' }})
}});
window.location = '/suite/compliance/issues';
}} catch (e) {{
alert('Failed to update issue');
}}
}}
function editIssue() {{
window.location = `/suite/compliance/issues/${{issueId}}/edit`;
}}
loadIssue();
</script>
</body>
</html>"#);
Html(html)
}
pub fn configure_compliance_ui_routes() -> Router<Arc<AppState>> {
Router::new()
.route("/suite/compliance", get(handle_compliance_dashboard_page))
.route("/suite/compliance/issues", get(handle_compliance_issues_page))
.route("/suite/compliance/issues/:id", get(handle_compliance_issue_detail_page))
}