1use base64::prelude::*;
18use serde_json::Value;
19
20use crate::admin::ops::{ExpiringEntry, OrderDetail};
21use crate::sqlite::account::{Account, pubkey_fingerprint};
22use crate::sqlite::admin_session::AdminSession;
23use crate::sqlite::admin_user::AdminUser;
24use crate::sqlite::eab::Eab;
25use crate::sqlite::order::{Order, rfc3339};
26
27#[must_use]
33pub fn profile_base_url(base_url: &str, profile: &str) -> String {
34 format!(
35 "{}{}/{profile}",
36 base_url.trim_end_matches('/'),
37 crate::PROFILE_PREFIX
38 )
39}
40
41#[must_use]
48pub fn render_account_json(account: &Account, base_url: &str) -> Value {
49 let mut object = account
50 .to_json(&profile_base_url(base_url, &account.profile))
51 .as_object()
52 .cloned()
53 .unwrap_or_default();
54 object.insert("id".to_string(), Value::String(account.id.clone()));
55 object.insert(
56 "profile".to_string(),
57 Value::String(account.profile.clone()),
58 );
59 object.insert(
60 "createdAt".to_string(),
61 Value::String(rfc3339(account.created_at)),
62 );
63 object.insert(
64 "pubkeyFingerprint".to_string(),
65 Value::String(pubkey_fingerprint(&account.pubkey)),
66 );
67 if let Some(seen) = account.last_seen_at {
68 object.insert("lastSeenAt".to_string(), Value::String(rfc3339(seen)));
69 }
70 for (key, value) in [
71 ("createdIp", account.created_ip.as_ref()),
72 ("createdPtr", account.created_ptr.as_ref()),
73 ("lastSeenIp", account.last_seen_ip.as_ref()),
74 ("lastSeenPtr", account.last_seen_ptr.as_ref()),
75 ] {
76 if let Some(value) = value {
77 object.insert(key.to_string(), Value::String(value.clone()));
78 }
79 }
80 Value::Object(object)
81}
82
83#[must_use]
85pub fn render_order_json(order: &Order, base_url: &str, authz_ids: &[String]) -> Value {
86 let mut object = order
87 .to_json(&profile_base_url(base_url, &order.profile), authz_ids)
88 .as_object()
89 .cloned()
90 .unwrap_or_default();
91 object.insert("id".to_string(), Value::String(order.id.clone()));
92 object.insert("profile".to_string(), Value::String(order.profile.clone()));
93 object.insert(
98 "accountId".to_string(),
99 Value::String(order.account_id.clone()),
100 );
101 object.insert(
102 "createdAt".to_string(),
103 Value::String(rfc3339(order.created_at)),
104 );
105 if let Some(serial) = order.cert_serial.as_ref() {
114 object.insert("certSerial".to_string(), Value::String(serial.clone()));
115 }
116 if let Some(not_after) = order.cert_not_after.filter(|value| *value >= 0) {
124 object.insert(
125 "certNotAfter".to_string(),
126 Value::String(rfc3339(not_after)),
127 );
128 }
129 if let Some(revoked_at) = order.revoked_at {
130 object.insert("revokedAt".to_string(), Value::String(rfc3339(revoked_at)));
131 if let Some(reason) = order.revocation_reason {
132 object.insert("revocationReason".to_string(), Value::from(reason));
133 }
134 }
135 Value::Object(object)
136}
137
138#[must_use]
140pub fn render_order_detail_json(detail: &OrderDetail, base_url: &str) -> Value {
141 let authz_ids: Vec<String> = detail
142 .authorizations
143 .iter()
144 .map(|(a, _)| a.id.clone())
145 .collect();
146 let profile_base = profile_base_url(base_url, &detail.order.profile);
147 let authorizations: Vec<Value> = detail
148 .authorizations
149 .iter()
150 .map(|(a, c)| a.to_json(&profile_base, c))
151 .collect();
152 let mut order = render_order_json(&detail.order, base_url, &authz_ids);
153 if let (Some(object), Some(pem)) = (order.as_object_mut(), detail.order.certificate.as_ref()) {
164 object.insert("certificatePem".to_string(), Value::String(pem.clone()));
165 }
166
167 let mut root = serde_json::Map::new();
168 root.insert("order".to_string(), order);
169 root.insert("authorizations".to_string(), Value::Array(authorizations));
170 Value::Object(root)
171}
172
173#[must_use]
187pub fn render_expiring_json(entry: &ExpiringEntry) -> Value {
188 let order = &entry.order;
189 let mut object = serde_json::Map::new();
190 object.insert("orderId".to_string(), Value::String(order.id.clone()));
191 object.insert("profile".to_string(), Value::String(order.profile.clone()));
192 object.insert(
193 "accountId".to_string(),
194 Value::String(order.account_id.clone()),
195 );
196 object.insert(
197 "certSerial".to_string(),
198 Value::String(order.cert_serial.clone().unwrap_or_default()),
199 );
200 object.insert(
201 "identifiers".to_string(),
202 Value::Array(
203 order
204 .identifiers
205 .iter()
206 .map(|identifier| Value::String(identifier.value.clone()))
207 .collect(),
208 ),
209 );
210 object.insert(
211 "notAfter".to_string(),
212 Value::String(rfc3339(order.cert_not_after.unwrap_or_default())),
213 );
214 object.insert(
215 "daysRemaining".to_string(),
216 Value::from(entry.days_remaining),
217 );
218 if let Some(superseded) = &entry.superseded_by {
219 object.insert(
220 "supersededBy".to_string(),
221 serde_json::json!({
222 "orderId": superseded.order_id,
223 "certSerial": superseded.cert_serial,
224 "notAfter": rfc3339(superseded.not_after),
225 "via": superseded.via,
226 }),
227 );
228 }
229 Value::Object(object)
230}
231
232#[must_use]
234pub fn render_eab_json(eab: &Eab) -> Value {
235 eab.to_json()
236}
237
238#[must_use]
240pub fn render_eab_created_json(eab: &Eab) -> Value {
241 let mut object = eab.to_json().as_object().cloned().unwrap_or_default();
242 object.insert(
243 "hmacKey".to_string(),
244 Value::String(BASE64_URL_SAFE_NO_PAD.encode(&eab.secret)),
245 );
246 Value::Object(object)
247}
248
249#[must_use]
255pub fn render_admin_user_json(user: &AdminUser) -> Value {
256 user.to_json()
257}
258
259#[must_use]
261pub fn render_admin_session_json(session: &AdminSession) -> Value {
262 session.to_json()
263}
264
265#[cfg(test)]
266mod tests {
267 use std::sync::Arc;
268
269 use super::*;
270 use crate::admin::ops::load_order_detail;
271 use crate::audit::ClientContext;
272 use crate::sqlite::authz::{Authorization, Challenge};
273 use crate::sqlite::db::Database;
274 use crate::sqlite::order::Identifier;
275 use crate::sqlite::status::OrderStatus;
276 use crate::testutil::{
277 account_id, account_seen_from, admin_session_fixture, admin_user_fixture, client_context,
278 order_fixture,
279 };
280
281 #[tokio::test]
282 async fn render_account_json_includes_id_and_base_fields() {
283 let db = Arc::new(Database::connect_in_memory().await.unwrap());
284 let account = account_seen_from(
285 &[1u8, 2, 3],
286 &client_context(Some("203.0.113.7"), Some("host.example.com")),
287 &db,
288 )
289 .await;
290
291 let json = render_account_json(&account, "http://localhost:3000");
292 assert_eq!(json["id"], account.id);
293 assert_eq!(json["status"], "valid");
294 assert!(json["orders"].as_str().unwrap().contains(&account.id));
295 assert!(json["pubkeyFingerprint"].is_string());
296 assert_eq!(json["createdIp"], "203.0.113.7");
297 assert_eq!(json["createdPtr"], "host.example.com");
298 assert_eq!(json["lastSeenIp"], "203.0.113.7");
299 assert_eq!(json["lastSeenPtr"], "host.example.com");
300 assert!(json["lastSeenAt"].as_str().unwrap().contains('T'));
301 }
302
303 #[tokio::test]
306 async fn render_account_json_omits_the_traceability_members_it_has_no_value_for() {
307 let db = Arc::new(Database::connect_in_memory().await.unwrap());
308 let account = account_seen_from(&[1u8, 2, 3], &ClientContext::default(), &db).await;
309
310 let json = render_account_json(&account, "http://localhost:3000");
311 let object = json.as_object().unwrap();
312 for key in ["createdIp", "createdPtr", "lastSeenIp", "lastSeenPtr"] {
313 assert!(!object.contains_key(key), "{key} in {json}");
314 }
315 }
316
317 #[test]
318 fn render_order_json_includes_id_and_authorizations() {
319 let order = order_fixture("acct", OrderStatus::Pending);
320 let authz_ids = vec!["authz-1".to_string()];
321 let json = render_order_json(&order, "http://localhost:3000", &authz_ids);
322 assert_eq!(json["id"], order.id);
323 assert_eq!(json["profile"], "default");
326 assert_eq!(json["accountId"], "acct");
329 assert_eq!(
330 json["authorizations"],
331 serde_json::json!(["http://localhost:3000/profile/default/authz/authz-1"])
332 );
333 }
334
335 #[tokio::test]
336 async fn render_order_detail_json_nests_authorizations_and_challenges() {
337 let db = Arc::new(Database::connect_in_memory().await.unwrap());
338 let acct = account_id(&db).await;
339 let order = Order::create(
340 "default",
341 &acct,
342 vec![Identifier::dns("example.com")],
343 crate::sqlite::nonce::now_secs() + 3600,
344 None,
345 None,
346 &db,
347 )
348 .await
349 .unwrap();
350 let authz = Authorization::create(
351 &order.id,
352 Identifier::dns("example.com"),
353 crate::sqlite::nonce::now_secs() + 3600,
354 &db,
355 )
356 .await
357 .unwrap();
358 Challenge::create(&authz.id, "http-01", &db).await.unwrap();
359
360 let detail = load_order_detail(&order.id, db).await.unwrap().unwrap();
361 let json = render_order_detail_json(&detail, "http://localhost:3000");
362 assert_eq!(json["order"]["id"], order.id);
363 assert_eq!(json["authorizations"].as_array().unwrap().len(), 1);
364 assert_eq!(
365 json["authorizations"][0]["challenges"]
366 .as_array()
367 .unwrap()
368 .len(),
369 1
370 );
371 }
372
373 #[tokio::test]
374 async fn render_eab_created_json_includes_the_hmac_key_and_line_json_does_not() {
375 let db = Arc::new(Database::connect_in_memory().await.unwrap());
376 let eab = Eab::create(None, None, &db).await.unwrap();
377
378 let created = render_eab_created_json(&eab);
379 assert!(created["hmacKey"].is_string());
380
381 let listed = render_eab_json(&eab);
382 assert!(listed.get("hmacKey").is_none());
383 }
384
385 #[test]
386 fn render_order_json_revoked_includes_reason_and_time() {
387 let mut order = order_fixture("acct", OrderStatus::Valid);
388 order.revoked_at = Some(1700000000);
389 order.revocation_reason = Some(1);
390 let json = render_order_json(&order, "http://localhost:3000", &[]);
391 assert_eq!(json["revokedAt"].as_str().unwrap().len(), 20);
392 assert_eq!(json["revocationReason"], 1);
393 }
394
395 #[test]
396 fn render_order_json_carries_the_cert_serial_and_omits_it_when_unissued() {
397 let mut order = order_fixture("acct", OrderStatus::Valid);
401 order.cert_serial = Some("03a7f1c9".to_string());
402 let json = render_order_json(&order, "http://localhost:3000", &[]);
403 assert_eq!(json["certSerial"], "03a7f1c9");
404
405 let unissued = order_fixture("acct", OrderStatus::Pending);
408 let json = render_order_json(&unissued, "http://localhost:3000", &[]);
409 assert!(json.get("certSerial").is_none());
410 }
411
412 #[test]
413 fn render_admin_user_json_omits_every_secret() {
414 let mut user = admin_user_fixture();
415 user.totp_secret = Some(vec![9, 9, 9]);
416 let json = render_admin_user_json(&user);
417 let rendered = json.to_string();
418 assert!(!rendered.contains("pbkdf2"));
419 assert!(!rendered.contains("totpSecret"));
420 assert_eq!(json["username"], "alice");
421 assert_eq!(json["totpEnabled"], true);
422 }
423
424 #[test]
425 fn render_admin_session_json_omits_the_hash_and_the_csrf_token() {
426 let json = render_admin_session_json(&admin_session_fixture());
427 let rendered = json.to_string();
428 assert!(!rendered.contains("0123456789abcdef0123456789abcdef"));
429 assert!(!rendered.contains("the-csrf-token"));
430 assert_eq!(json["id"], "01234567");
431 assert_eq!(json["state"], "active");
432 }
433}