src/front_certs.rs
//! **Admin → Certificates** (owner 2026-10-25: certificates must fix themselves) — the
//! pane's pure decisions, host-tested; `pages/front_certs.rs` lays them out.
//!
//! Thin: the FRONT says everything (holger-estate-front `certs_page`, edda estate-front's
//! `web::status`): valid until, next attempt, last reason, whether "try now" is taken, the
//! one-hour alert. The pane shows those lines and, where the front allows it, the button.
#![cfg_attr(not(target_arch = "wasm32"), allow(dead_code))]
use serde::{Deserialize, Serialize};
/// The front's routes (not under `/-/`, which is the primary's).
pub const CERTS_PATH: &str = "/front/certs";
pub const TRY_PATH: &str = "/front/certs/try";
/// One certificate as the front says it.
#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
#[serde(default)]
pub struct Cert {
pub host: String,
pub hosts: Vec<String>,
pub lines: Vec<String>,
pub try_now: bool,
pub alert: Option<String>,
}
/// `GET /front/certs`.
#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
#[serde(default)]
pub struct Certs {
pub certificates: Vec<Cert>,
}
/// Pure: **the pane's reading of the front's answer** — the certificates, or the words why
/// there are none to show (a console not served by an estate front has no such route).
pub fn read(status: u16, body: &str) -> Result<Certs, String> {
match status {
200 => serde_json::from_str(body).map_err(|e| format!("the front's answer is not the certificate report: {e}")),
404 => Err("this console is not served by an estate front: it has no certificates of its own to show".into()),
_ => Err(if body.trim().is_empty() { format!("the front answered HTTP {status}") } else { body.trim().to_string() }),
}
}
/// Pure: **"try now"** — `(may be pressed, why not)`: only where the front allows it (the
/// front refuses while a failed validation's wait runs; the pane is already the admin's).
pub fn button(c: &Cert) -> (bool, String) {
if c.try_now {
(true, String::new())
} else {
(false, "the last attempt failed Let's Encrypt's validation; the front waits it out".into())
}
}
/// Pure: the body of a try now.
pub fn try_body(c: &Cert) -> serde_json::Value {
serde_json::json!({ "host": c.host })
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn the_pane_shows_the_fronts_words_and_offers_try_now_only_where_the_front_allows_it() {
let c = read(200, r#"{"certificates":[{"host":"holger.example","hosts":["holger.example"],"lines":["valid until: no valid certificate","last reason: dns: holger.example does not point at this box (it resolves to nothing)"],"try_now":true,"alert":null}]}"#).unwrap();
let one = &c.certificates[0];
assert_eq!(one.lines.len(), 2);
assert_eq!(button(one), (true, String::new()));
assert_eq!(try_body(one)["host"], "holger.example");
let refused = Cert { try_now: false, ..one.clone() };
assert!(!button(&refused).0 && button(&refused).1.contains("validation"));
assert!(read(404, "").unwrap_err().contains("not served by an estate front"));
assert!(read(403, "REFUSED [front-certs-admin] the certificates are an admin's (you are reader)").unwrap_err().contains("front-certs-admin"));
assert!(read(200, "not json").is_err());
}
}