auths_sdk/presentation/
html.rs1use crate::ports::git::{CommitRecord, SignatureStatus};
4use crate::workflows::audit::AuditSummary;
5
6pub fn render_audit_html(
20 generated_at: &str,
21 repository: &str,
22 summary: &AuditSummary,
23 commits: &[CommitRecord],
24) -> String {
25 let signed_pct = if summary.total_commits > 0 {
26 (summary.signed_commits as f64 / summary.total_commits as f64) * 100.0
27 } else {
28 0.0
29 };
30
31 let rows = commits
32 .iter()
33 .map(render_commit_row)
34 .collect::<Vec<_>>()
35 .join("\n");
36
37 format!(
38 r#"<!DOCTYPE html>
39<html lang="en">
40<head>
41 <meta charset="UTF-8">
42 <meta name="viewport" content="width=device-width, initial-scale=1.0">
43 <title>Audit Report</title>
44 <style>
45 body {{ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; margin: 2rem; }}
46 h1 {{ color: #1a1a1a; }}
47 .summary {{ background: #f5f5f5; padding: 1rem; border-radius: 8px; margin: 1rem 0; }}
48 .stat {{ display: inline-block; margin-right: 2rem; }}
49 .stat-value {{ font-size: 2rem; font-weight: bold; color: #0066cc; }}
50 .stat-label {{ color: #666; }}
51 table {{ width: 100%; border-collapse: collapse; margin-top: 1rem; }}
52 th, td {{ padding: 0.5rem; text-align: left; border-bottom: 1px solid #ddd; }}
53 th {{ background: #f5f5f5; }}
54 .signed {{ color: #22c55e; }}
55 .unsigned {{ color: #ef4444; }}
56 .verified {{ color: #22c55e; }}
57 .unverified {{ color: #f59e0b; }}
58 </style>
59</head>
60<body>
61 <h1>Audit Report</h1>
62 <p>Generated: {generated_at}</p>
63 <p>Repository: {repository}</p>
64
65 <div class="summary">
66 <div class="stat">
67 <div class="stat-value">{total}</div>
68 <div class="stat-label">Total Commits</div>
69 </div>
70 <div class="stat">
71 <div class="stat-value signed">{signed}</div>
72 <div class="stat-label">Signed ({signed_pct:.0}%)</div>
73 </div>
74 <div class="stat">
75 <div class="stat-value unsigned">{unsigned}</div>
76 <div class="stat-label">Unsigned</div>
77 </div>
78 </div>
79
80 <table>
81 <thead>
82 <tr>
83 <th>Hash</th>
84 <th>Date</th>
85 <th>Author</th>
86 <th>Message</th>
87 <th>Method</th>
88 <th>Verified</th>
89 </tr>
90 </thead>
91 <tbody>
92 {rows}
93 </tbody>
94 </table>
95</body>
96</html>"#,
97 generated_at = html_escape::encode_text(generated_at),
98 repository = html_escape::encode_text(repository),
99 total = summary.total_commits,
100 signed = summary.signed_commits,
101 unsigned = summary.unsigned_commits,
102 signed_pct = signed_pct,
103 rows = rows,
104 )
105}
106
107fn render_commit_row(c: &CommitRecord) -> String {
108 let (signing_method, is_signed, is_verified) = classify_signature(&c.signature_status);
109 let method_class = if is_signed { "signed" } else { "unsigned" };
110 let verified_class = if is_verified {
111 "verified"
112 } else {
113 "unverified"
114 };
115 let verified_text = if !is_signed {
116 "-"
117 } else if is_verified {
118 "Yes"
119 } else {
120 "No"
121 };
122 let date = if c.timestamp.len() >= 10 {
123 &c.timestamp[..10]
124 } else {
125 &c.timestamp
126 };
127
128 format!(
129 r#"<tr>
130 <td><code>{hash}</code></td>
131 <td>{date}</td>
132 <td>{author}</td>
133 <td>{message}</td>
134 <td class="{method_class}">{method}</td>
135 <td class="{verified_class}">{verified}</td>
136 </tr>"#,
137 hash = html_escape::encode_text(&c.hash),
138 date = html_escape::encode_text(date),
139 author = html_escape::encode_text(&c.author_name),
140 message = html_escape::encode_text(&c.message),
141 method_class = method_class,
142 method = html_escape::encode_text(signing_method),
143 verified_class = verified_class,
144 verified = verified_text,
145 )
146}
147
148fn classify_signature(status: &SignatureStatus) -> (&'static str, bool, bool) {
149 match status {
150 SignatureStatus::AuthsSigned { .. } => ("auths", true, true),
151 SignatureStatus::SshSigned => ("ssh", true, false),
152 SignatureStatus::GpgSigned { verified } => ("gpg", true, *verified),
153 SignatureStatus::Unsigned => ("none", false, false),
154 SignatureStatus::InvalidSignature { .. } => ("invalid", true, false),
155 }
156}