openauth_plugins/captcha/
options.rs1use 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}