mas_config/sections/
captcha.rs

1// Copyright 2024, 2025 New Vector Ltd.
2// Copyright 2024 The Matrix.org Foundation C.I.C.
3//
4// SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
5// Please see LICENSE files in the repository root for full details.
6
7use schemars::JsonSchema;
8use serde::{Deserialize, Serialize, de::Error};
9
10use crate::ConfigurationSection;
11
12/// Which service should be used for CAPTCHA protection
13#[derive(Clone, Copy, Debug, Deserialize, JsonSchema, Serialize)]
14pub enum CaptchaServiceKind {
15    /// Use Google's reCAPTCHA v2 API
16    #[serde(rename = "recaptcha_v2")]
17    RecaptchaV2,
18
19    /// Use Cloudflare Turnstile
20    #[serde(rename = "cloudflare_turnstile")]
21    CloudflareTurnstile,
22
23    /// Use ``HCaptcha``
24    #[serde(rename = "hcaptcha")]
25    HCaptcha,
26}
27
28/// Configuration section to setup CAPTCHA protection on a few operations
29#[derive(Clone, Debug, Deserialize, JsonSchema, Serialize, Default)]
30pub struct CaptchaConfig {
31    /// Which service should be used for CAPTCHA protection
32    #[serde(skip_serializing_if = "Option::is_none")]
33    pub service: Option<CaptchaServiceKind>,
34
35    /// The site key to use
36    #[serde(skip_serializing_if = "Option::is_none")]
37    pub site_key: Option<String>,
38
39    /// The secret key to use
40    #[serde(skip_serializing_if = "Option::is_none")]
41    pub secret_key: Option<String>,
42}
43
44impl CaptchaConfig {
45    /// Returns true if the configuration is the default one
46    pub(crate) fn is_default(&self) -> bool {
47        self.service.is_none() && self.site_key.is_none() && self.secret_key.is_none()
48    }
49}
50
51impl ConfigurationSection for CaptchaConfig {
52    const PATH: Option<&'static str> = Some("captcha");
53
54    fn validate(
55        &self,
56        figment: &figment::Figment,
57    ) -> Result<(), Box<dyn std::error::Error + Send + Sync + 'static>> {
58        let metadata = figment.find_metadata(Self::PATH.unwrap());
59
60        let error_on_field = |mut error: figment::error::Error, field: &'static str| {
61            error.metadata = metadata.cloned();
62            error.profile = Some(figment::Profile::Default);
63            error.path = vec![Self::PATH.unwrap().to_owned(), field.to_owned()];
64            error
65        };
66
67        let missing_field = |field: &'static str| {
68            error_on_field(figment::error::Error::missing_field(field), field)
69        };
70
71        if let Some(CaptchaServiceKind::RecaptchaV2) = self.service {
72            if self.site_key.is_none() {
73                return Err(missing_field("site_key").into());
74            }
75
76            if self.secret_key.is_none() {
77                return Err(missing_field("secret_key").into());
78            }
79        }
80
81        Ok(())
82    }
83}