mcaptcha_api_rs/
lib.rs

1// SPDX-FileCopyrightText: 2024 Aravinth Manivannan <realaravinth@batsense.net>
2//
3// SPDX-License-Identifier: Apache-2.0
4// SPDX-License-Identifier: MIT
5
6//! # mcaptcha-api-rs: Library to interact with with mCaptcha API
7//!
8//! This library provides a convenient interface to validate [mCaptcha authorization
9//! tokens](https://mcaptcha.org/docs/webmasters/terminology#authorization-token) presented by
10//! Visitors against your mCaptcha instances. It uses [reqwest](https://crates.io/crates/reqwest),
11//! and `native-tls` under the hood to communicate with the API.
12//! ```rust,ignore
13//!  use url::Url;
14//!  use mcaptcha_api_rs::MCaptcha;
15//!
16//!  let mcaptcha = MCaptcha::new("sitekeyfromdashboard", "secretfromdashboadr", Url::parse("https://mcaptcha.example.com").unwrap());
17//!  assert!(mcaptcha.verify("authorizationtokenfromvisitor").await.unwrap());
18//!  ```
19
20use reqwest::Client;
21use serde::{Deserialize, Serialize};
22use url::Url;
23
24/// API Client class
25pub struct MCaptcha {
26    client: Client,
27    sitekey: String,
28    account_secret: String,
29    verify_path: Url,
30}
31
32impl MCaptcha {
33    /// Create new instance of API client
34    ///
35    /// Parameters:
36    /// - [`sitekey`](https://mcaptcha.org/docs/webmasters/terminology#sitekey):  unique identifier of
37    /// captcha configuration. Available on mCaptcha dashboard.
38    /// - `account_secret`: used for authentication. Available on the settings page of mCaptcha dashboard
39    /// - `instance_url`: the URL of your mCaptcha instance
40    ///
41    pub fn new<T: Into<String>>(sitekey: T, account_secret: T, instance_url: Url) -> Self {
42        let mut verify_path = instance_url.clone();
43        verify_path.set_path("/api/v1/pow/siteverify");
44        Self {
45            sitekey: sitekey.into(),
46            account_secret: account_secret.into(),
47            verify_path,
48            client: Client::new(),
49        }
50    }
51
52    /// Verify authorization token presented by visitor against mCaptcha server
53    pub async fn verify(&self, token: &str) -> Result<bool, reqwest::Error> {
54        let payload = CaptchaVerfiyPayload::from_ctx(self, token);
55        let res: CaptchaVerifyResp = self
56            .client
57            .post(self.verify_path.clone())
58            .json(&payload)
59            .send()
60            .await?
61            .json()
62            .await?;
63
64        Ok(res.valid)
65    }
66}
67
68#[derive(Default, Debug, PartialEq, Clone, Serialize, Deserialize)]
69struct CaptchaVerifyResp {
70    valid: bool,
71}
72
73#[derive(Default, Debug, PartialEq, Clone, Serialize, Deserialize)]
74struct CaptchaVerfiyPayload<'a> {
75    token: &'a str,
76    key: &'a str,
77    secret: &'a str,
78}
79
80impl<'a> CaptchaVerfiyPayload<'a> {
81    fn from_ctx(m: &'a MCaptcha, token: &'a str) -> Self {
82        Self {
83            key: &m.sitekey,
84            token,
85            secret: &m.account_secret,
86        }
87    }
88}
89
90#[cfg(test)]
91mod tests {
92    use std::env;
93
94    use super::*;
95
96    #[actix_rt::test]
97    async fn verify_works() {
98        dotenv::from_filename(".env.local").ok();
99
100        let instance_url = env::var("INSTANCE_URL").expect("instance url not set");
101        let sitekey = env::var("SITEKEY").expect("sitekey not set");
102        let secret = env::var("SECRET").expect("secret not set");
103        let token = env::var("TOKEN").expect("token not set");
104
105        println!("token={token}");
106        println!("sitekey={sitekey}");
107        println!("secret={secret}");
108        println!("instance_url={instance_url}");
109
110        let mcaptcha = MCaptcha::new(
111            sitekey,
112            secret,
113            Url::parse(&instance_url).expect("instance_url is not URL"),
114        );
115        assert!(mcaptcha.verify(&token).await.unwrap());
116        assert!(!mcaptcha.verify("foo").await.unwrap());
117    }
118}