Skip to main content

openauth_plugins/captcha/
options.rs

1//! CAPTCHA options.
2
3use serde::{Deserialize, Serialize};
4
5use super::error::CaptchaConfigError;
6
7pub const DEFAULT_ENDPOINTS: &[&str] = &[
8    "/sign-up/email",
9    "/sign-in/email",
10    "/request-password-reset",
11];
12
13#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
14pub enum CaptchaProvider {
15    #[serde(rename = "cloudflare-turnstile")]
16    CloudflareTurnstile,
17    #[serde(rename = "google-recaptcha")]
18    GoogleRecaptcha,
19    #[serde(rename = "hcaptcha")]
20    HCaptcha,
21    #[serde(rename = "captchafox")]
22    CaptchaFox,
23}
24
25impl CaptchaProvider {
26    pub fn site_verify_url(self) -> &'static str {
27        match self {
28            Self::CloudflareTurnstile => {
29                "https://challenges.cloudflare.com/turnstile/v0/siteverify"
30            }
31            Self::GoogleRecaptcha => "https://www.google.com/recaptcha/api/siteverify",
32            Self::HCaptcha => "https://api.hcaptcha.com/siteverify",
33            Self::CaptchaFox => "https://api.captchafox.com/siteverify",
34        }
35    }
36}
37
38#[derive(Debug, Clone, Serialize, Deserialize)]
39pub struct CaptchaOptions {
40    pub provider: CaptchaProvider,
41    #[serde(skip_serializing)]
42    pub secret_key: String,
43    #[serde(default)]
44    pub endpoints: Vec<String>,
45    #[serde(default)]
46    pub site_verify_url_override: Option<String>,
47    #[serde(default)]
48    pub min_score: Option<f64>,
49    #[serde(default)]
50    pub site_key: Option<String>,
51    #[serde(skip)]
52    pub http_client: Option<reqwest::Client>,
53}
54
55impl CaptchaOptions {
56    pub fn with_provider(provider: CaptchaProvider, secret_key: impl Into<String>) -> Self {
57        Self {
58            provider,
59            secret_key: secret_key.into(),
60            endpoints: Vec::new(),
61            site_verify_url_override: None,
62            min_score: None,
63            site_key: None,
64            http_client: None,
65        }
66    }
67
68    pub fn cloudflare_turnstile(secret_key: impl Into<String>) -> Self {
69        Self::with_provider(CaptchaProvider::CloudflareTurnstile, secret_key)
70    }
71
72    pub fn google_recaptcha(secret_key: impl Into<String>) -> Self {
73        Self::with_provider(CaptchaProvider::GoogleRecaptcha, secret_key)
74    }
75
76    pub fn hcaptcha(secret_key: impl Into<String>) -> Self {
77        Self::with_provider(CaptchaProvider::HCaptcha, secret_key)
78    }
79
80    pub fn captchafox(secret_key: impl Into<String>) -> Self {
81        Self::with_provider(CaptchaProvider::CaptchaFox, secret_key)
82    }
83
84    #[must_use]
85    pub fn endpoints<I, S>(mut self, endpoints: I) -> Self
86    where
87        I: IntoIterator<Item = S>,
88        S: Into<String>,
89    {
90        self.endpoints = endpoints.into_iter().map(Into::into).collect();
91        self
92    }
93
94    #[must_use]
95    pub fn site_verify_url_override(mut self, url: impl Into<String>) -> Self {
96        self.site_verify_url_override = Some(url.into());
97        self
98    }
99
100    #[must_use]
101    pub fn min_score(mut self, min_score: f64) -> Self {
102        self.min_score = Some(min_score);
103        self
104    }
105
106    #[must_use]
107    pub fn site_key(mut self, site_key: impl Into<String>) -> Self {
108        self.site_key = Some(site_key.into());
109        self
110    }
111
112    #[must_use]
113    pub fn http_client(mut self, http_client: reqwest::Client) -> Self {
114        self.http_client = Some(http_client);
115        self
116    }
117
118    pub(crate) fn validate(&self) -> Result<(), CaptchaConfigError> {
119        if self.secret_key.trim().is_empty() {
120            return Err(CaptchaConfigError::MissingSecretKey);
121        }
122        Ok(())
123    }
124
125    pub(crate) fn with_defaults(mut self) -> Self {
126        if self.endpoints.is_empty() {
127            self.endpoints = DEFAULT_ENDPOINTS
128                .iter()
129                .map(|endpoint| (*endpoint).to_owned())
130                .collect();
131        }
132        self
133    }
134
135    pub(crate) fn site_verify_url(&self) -> &str {
136        self.site_verify_url_override
137            .as_deref()
138            .unwrap_or_else(|| self.provider.site_verify_url())
139    }
140
141    pub(crate) fn http_client_ref(&self) -> reqwest::Client {
142        self.http_client.clone().unwrap_or_default()
143    }
144
145    pub(crate) fn google_min_score(&self) -> f64 {
146        self.min_score.unwrap_or(0.5)
147    }
148}