1use std::io::BufRead;
7use std::sync::Arc;
8use std::time::Duration;
9
10use clap::Subcommand;
11
12use crate::cli::CliError;
13use acme_proxy_admin::admin;
14use acme_proxy_core::config::Config;
15use acme_proxy_jobs::auditor::admin as audit_admin;
16use acme_proxy_store::db::Database;
17use acme_proxy_store::nonce::Nonce;
18
19#[derive(Subcommand)]
20pub enum NonceCommand {
21 Cleanup {
23 #[arg(long = "ttl-seconds")]
25 ttl_seconds: Option<u64>,
26 },
27 Count {
29 #[arg(long)]
31 json: bool,
32 },
33}
34
35pub async fn run_nonce_command(
36 command: NonceCommand,
37 yes: bool,
38 reader: &mut impl BufRead,
39 config: &Config,
40 database: Arc<Database>,
41) -> Result<(), CliError> {
42 match command {
43 NonceCommand::Cleanup { ttl_seconds } => {
44 let ttl = Duration::from_secs(ttl_seconds.unwrap_or(config.nonce.ttl_seconds));
45 match admin::confirm_cleanup_nonces(ttl, yes, reader, database.clone()).await? {
46 None => println!("Cancelled."),
47 Some(removed) => {
48 if removed > 0 {
52 audit_admin::record_cli_action(&database, |actor, client| {
53 audit_admin::nonce_cleanup_completed(actor, client, removed)
54 })
55 .await;
56 }
57 println!("Removed {removed} nonce(s).");
58 }
59 }
60 }
61 NonceCommand::Count { json } => {
62 let count = Nonce::count(&database).await?;
67 let ttl = config.nonce.ttl_seconds;
68 if json {
69 println!("{}", admin::render_nonce_stats_json(count, ttl));
70 } else {
71 println!("{count} nonce(s), ttl {ttl}s.");
72 }
73 }
74 }
75 Ok(())
76}
77
78#[cfg(test)]
79mod tests {
80 use super::*;
81
82 #[tokio::test]
85 async fn count_reports_the_table_and_the_configured_ttl() {
86 let database = Arc::new(Database::connect_in_memory().await.unwrap());
87 let mut config = Config::default();
88 config.nonce.ttl_seconds = 42;
89
90 for _ in 0..3 {
91 Nonce::new().save(&database).await.unwrap();
92 }
93 assert_eq!(Nonce::count(&database).await.unwrap(), 3);
94
95 assert_eq!(
96 admin::render_nonce_stats_json(3, config.nonce.ttl_seconds),
97 serde_json::json!({ "count": 3, "ttlSeconds": 42 }),
98 );
99
100 for json in [false, true] {
101 let mut reader: &[u8] = &[];
102 run_nonce_command(
103 NonceCommand::Count { json },
104 true,
105 &mut reader,
106 &config,
107 database.clone(),
108 )
109 .await
110 .unwrap();
111 }
112 }
113
114 #[tokio::test]
117 async fn cleanup_honours_the_configured_ttl_and_the_prompt() {
118 let database = Arc::new(
119 acme_proxy_store::db::Database::connect_in_memory()
120 .await
121 .unwrap(),
122 );
123 let mut config = Config::default();
124 config.nonce.ttl_seconds = 1;
125
126 let mut declined: &[u8] = b"n\n";
127 run_nonce_command(
128 NonceCommand::Cleanup { ttl_seconds: None },
129 false,
130 &mut declined,
131 &config,
132 database.clone(),
133 )
134 .await
135 .unwrap();
136
137 let mut reader: &[u8] = &[];
138 run_nonce_command(
139 NonceCommand::Cleanup {
140 ttl_seconds: Some(60),
141 },
142 true,
143 &mut reader,
144 &config,
145 database,
146 )
147 .await
148 .unwrap();
149 }
150}