rustauth_plugins/captcha/
options.rs1use rustauth_core::error::RustAuthError;
4use serde::{Deserialize, Serialize};
5
6use super::error::CaptchaConfigError;
7
8pub const DEFAULT_ENDPOINTS: &[&str] = &[
9 "/sign-up/email",
10 "/sign-in/email",
11 "/request-password-reset",
12];
13
14#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
15pub enum CaptchaProvider {
16 #[serde(rename = "cloudflare-turnstile")]
17 CloudflareTurnstile,
18 #[serde(rename = "google-recaptcha")]
19 GoogleRecaptcha,
20 #[serde(rename = "hcaptcha")]
21 HCaptcha,
22 #[serde(rename = "captchafox")]
23 CaptchaFox,
24}
25
26impl CaptchaProvider {
27 pub fn site_verify_url(self) -> &'static str {
28 match self {
29 Self::CloudflareTurnstile => {
30 "https://challenges.cloudflare.com/turnstile/v0/siteverify"
31 }
32 Self::GoogleRecaptcha => "https://www.google.com/recaptcha/api/siteverify",
33 Self::HCaptcha => "https://api.hcaptcha.com/siteverify",
34 Self::CaptchaFox => "https://api.captchafox.com/siteverify",
35 }
36 }
37}
38
39#[derive(Debug, Clone, Serialize, Deserialize)]
40pub struct CaptchaOptions {
41 pub provider: CaptchaProvider,
42 #[serde(skip_serializing)]
43 pub secret_key: String,
44 #[serde(default)]
45 pub endpoints: Vec<String>,
46 #[serde(default)]
47 pub site_verify_url_override: Option<String>,
48 #[serde(default)]
49 pub min_score: Option<f64>,
50 #[serde(default)]
51 pub site_key: Option<String>,
52 #[serde(skip)]
53 pub http_client: Option<reqwest::Client>,
54}
55
56impl CaptchaOptions {
57 #[must_use]
58 pub fn builder() -> CaptchaOptionsBuilder {
59 CaptchaOptionsBuilder::default()
60 }
61
62 pub fn with_provider(provider: CaptchaProvider, secret_key: impl Into<String>) -> Self {
63 Self {
64 provider,
65 secret_key: secret_key.into(),
66 endpoints: Vec::new(),
67 site_verify_url_override: None,
68 min_score: None,
69 site_key: None,
70 http_client: None,
71 }
72 }
73
74 pub fn cloudflare_turnstile(secret_key: impl Into<String>) -> Self {
75 Self::with_provider(CaptchaProvider::CloudflareTurnstile, secret_key)
76 }
77
78 pub fn google_recaptcha(secret_key: impl Into<String>) -> Self {
79 Self::with_provider(CaptchaProvider::GoogleRecaptcha, secret_key)
80 }
81
82 pub fn hcaptcha(secret_key: impl Into<String>) -> Self {
83 Self::with_provider(CaptchaProvider::HCaptcha, secret_key)
84 }
85
86 pub fn captchafox(secret_key: impl Into<String>) -> Self {
87 Self::with_provider(CaptchaProvider::CaptchaFox, secret_key)
88 }
89
90 #[must_use]
91 pub fn endpoints<I, S>(mut self, endpoints: I) -> Self
92 where
93 I: IntoIterator<Item = S>,
94 S: Into<String>,
95 {
96 self.endpoints = endpoints.into_iter().map(Into::into).collect();
97 self
98 }
99
100 #[must_use]
101 pub fn site_verify_url_override(mut self, url: impl Into<String>) -> Self {
102 self.site_verify_url_override = Some(url.into());
103 self
104 }
105
106 #[must_use]
107 pub fn min_score(mut self, min_score: f64) -> Self {
108 self.min_score = Some(min_score);
109 self
110 }
111
112 #[must_use]
113 pub fn site_key(mut self, site_key: impl Into<String>) -> Self {
114 self.site_key = Some(site_key.into());
115 self
116 }
117
118 #[must_use]
119 pub fn http_client(mut self, http_client: reqwest::Client) -> Self {
120 self.http_client = Some(http_client);
121 self
122 }
123
124 pub(crate) fn validate(&self) -> Result<(), CaptchaConfigError> {
125 if self.secret_key.trim().is_empty() {
126 return Err(CaptchaConfigError::MissingSecretKey);
127 }
128 Ok(())
129 }
130
131 pub(crate) fn with_defaults(mut self) -> Self {
132 if self.endpoints.is_empty() {
133 self.endpoints = DEFAULT_ENDPOINTS
134 .iter()
135 .map(|endpoint| (*endpoint).to_owned())
136 .collect();
137 }
138 self
139 }
140
141 pub(crate) fn site_verify_url(&self) -> &str {
142 self.site_verify_url_override
143 .as_deref()
144 .unwrap_or_else(|| self.provider.site_verify_url())
145 }
146
147 pub(crate) fn http_client_ref(&self) -> reqwest::Client {
148 self.http_client.clone().unwrap_or_default()
149 }
150
151 pub(crate) fn google_min_score(&self) -> f64 {
152 self.min_score.unwrap_or(0.5)
153 }
154}
155
156#[derive(Debug, Clone, Default)]
157pub struct CaptchaOptionsBuilder {
158 provider: Option<CaptchaProvider>,
159 secret_key: Option<String>,
160 endpoints: Option<Vec<String>>,
161 site_verify_url_override: Option<Option<String>>,
162 min_score: Option<Option<f64>>,
163 site_key: Option<Option<String>>,
164 http_client: Option<Option<reqwest::Client>>,
165}
166
167impl CaptchaOptionsBuilder {
168 #[must_use]
169 pub fn provider(mut self, provider: CaptchaProvider) -> Self {
170 self.provider = Some(provider);
171 self
172 }
173
174 #[must_use]
175 pub fn secret_key(mut self, secret_key: impl Into<String>) -> Self {
176 self.secret_key = Some(secret_key.into());
177 self
178 }
179
180 #[must_use]
181 pub fn endpoints(mut self, endpoints: Vec<String>) -> Self {
182 self.endpoints = Some(endpoints);
183 self
184 }
185
186 #[must_use]
187 pub fn site_verify_url_override(mut self, url: impl Into<String>) -> Self {
188 self.site_verify_url_override = Some(Some(url.into()));
189 self
190 }
191
192 #[must_use]
193 pub fn min_score(mut self, min_score: f64) -> Self {
194 self.min_score = Some(Some(min_score));
195 self
196 }
197
198 #[must_use]
199 pub fn site_key(mut self, site_key: impl Into<String>) -> Self {
200 self.site_key = Some(Some(site_key.into()));
201 self
202 }
203
204 #[must_use]
205 pub fn http_client(mut self, http_client: reqwest::Client) -> Self {
206 self.http_client = Some(Some(http_client));
207 self
208 }
209
210 pub fn build(self) -> Result<CaptchaOptions, RustAuthError> {
211 let provider = self.provider.ok_or_else(|| {
212 RustAuthError::InvalidConfig("captcha provider is required".to_owned())
213 })?;
214 let secret_key = self.secret_key.ok_or_else(|| {
215 RustAuthError::InvalidConfig("captcha secret_key is required".to_owned())
216 })?;
217 let options = CaptchaOptions {
218 provider,
219 secret_key,
220 endpoints: self.endpoints.unwrap_or_default(),
221 site_verify_url_override: self.site_verify_url_override.unwrap_or(None),
222 min_score: self.min_score.unwrap_or(None),
223 site_key: self.site_key.unwrap_or(None),
224 http_client: self.http_client.unwrap_or(None),
225 };
226 options
227 .validate()
228 .map_err(|error| RustAuthError::InvalidConfig(error.to_string()))?;
229 Ok(options)
230 }
231}