1use std::net::IpAddr;
6
7use serde::Deserialize;
8
9use crate::state::AppState;
10
11#[derive(Debug, Clone, Copy, PartialEq, Eq)]
12pub enum Verdict {
13 Passed,
15 Failed,
17 Unavailable,
19}
20
21#[derive(Deserialize)]
22struct SiteVerify {
23 success: bool,
24}
25
26pub async fn verify(state: &AppState, token: Option<&str>, ip: Option<IpAddr>) -> Verdict {
27 let Some(cfg) = &state.config.turnstile else {
28 return Verdict::Passed;
29 };
30 let Some(token) = token
32 .map(str::trim)
33 .filter(|t| !t.is_empty() && t.len() <= 2048)
34 else {
35 return Verdict::Failed;
36 };
37 let mut form = vec![
38 ("secret", cfg.secret.clone()),
39 ("response", token.to_owned()),
40 ];
41 if let Some(ip) = ip {
42 form.push(("remoteip", ip.to_string()));
43 }
44 let response = state
45 .http
46 .post(cfg.verify_url.clone())
47 .form(&form)
48 .send()
49 .await
50 .and_then(|r| r.error_for_status());
51 let body = match response {
52 Ok(r) => r.json::<SiteVerify>().await,
53 Err(e) => {
54 tracing::warn!(error = %e, "turnstile verification unavailable");
55 return Verdict::Unavailable;
56 }
57 };
58 match body {
59 Ok(v) if v.success => Verdict::Passed,
60 Ok(_) => Verdict::Failed,
61 Err(e) => {
62 tracing::warn!(error = %e, "turnstile verification returned an unreadable answer");
63 Verdict::Unavailable
64 }
65 }
66}