mcaptcha-api-rs 0.1.0

HTTP Library to interact with mCaptcha API
Documentation
// SPDX-FileCopyrightText: 2024 Aravinth Manivannan <realaravinth@batsense.net>
//
// SPDX-License-Identifier: Apache-2.0
// SPDX-License-Identifier: MIT

//! # mcaptcha-api-rs: Library to interact with with mCaptcha API
//!
//! This library provides a convenient interface to validate [mCaptcha authorization
//! tokens](https://mcaptcha.org/docs/webmasters/terminology#authorization-token) presented by
//! Visitors against your mCaptcha instances. It uses [reqwest](https://crates.io/crates/reqwest),
//! and `native-tls` under the hood to communicate with the API.
//! ```rust,ignore
//!  use url::Url;
//!  use mcaptcha_api_rs::MCaptcha;
//!
//!  let mcaptcha = MCaptcha::new("sitekeyfromdashboard", "secretfromdashboadr", Url::parse("https://mcaptcha.example.com").unwrap());
//!  assert!(mcaptcha.verify("authorizationtokenfromvisitor").await.unwrap());
//!  ```

use reqwest::Client;
use serde::{Deserialize, Serialize};
use url::Url;

/// API Client class
pub struct MCaptcha {
    client: Client,
    sitekey: String,
    account_secret: String,
    verify_path: Url,
}

impl MCaptcha {
    /// Create new instance of API client
    ///
    /// Parameters:
    /// - [`sitekey`](https://mcaptcha.org/docs/webmasters/terminology#sitekey):  unique identifier of
    /// captcha configuration. Available on mCaptcha dashboard.
    /// - `account_secret`: used for authentication. Available on the settings page of mCaptcha dashboard
    /// - `instance_url`: the URL of your mCaptcha instance
    ///
    pub fn new<T: Into<String>>(sitekey: T, account_secret: T, instance_url: Url) -> Self {
        let mut verify_path = instance_url.clone();
        verify_path.set_path("/api/v1/pow/siteverify");
        Self {
            sitekey: sitekey.into(),
            account_secret: account_secret.into(),
            verify_path,
            client: Client::new(),
        }
    }

    /// Verify authorization token presented by visitor against mCaptcha server
    pub async fn verify(&self, token: &str) -> Result<bool, reqwest::Error> {
        let payload = CaptchaVerfiyPayload::from_ctx(self, token);
        let res: CaptchaVerifyResp = self
            .client
            .post(self.verify_path.clone())
            .json(&payload)
            .send()
            .await?
            .json()
            .await?;

        Ok(res.valid)
    }
}

#[derive(Default, Debug, PartialEq, Clone, Serialize, Deserialize)]
struct CaptchaVerifyResp {
    valid: bool,
}

#[derive(Default, Debug, PartialEq, Clone, Serialize, Deserialize)]
struct CaptchaVerfiyPayload<'a> {
    token: &'a str,
    key: &'a str,
    secret: &'a str,
}

impl<'a> CaptchaVerfiyPayload<'a> {
    fn from_ctx(m: &'a MCaptcha, token: &'a str) -> Self {
        Self {
            key: &m.sitekey,
            token,
            secret: &m.account_secret,
        }
    }
}

#[cfg(test)]
mod tests {
    use std::env;

    use super::*;

    #[actix_rt::test]
    async fn verify_works() {
        dotenv::from_filename(".env.local").ok();

        let instance_url = env::var("INSTANCE_URL").expect("instance url not set");
        let sitekey = env::var("SITEKEY").expect("sitekey not set");
        let secret = env::var("SECRET").expect("secret not set");
        let token = env::var("TOKEN").expect("token not set");

        println!("token={token}");
        println!("sitekey={sitekey}");
        println!("secret={secret}");
        println!("instance_url={instance_url}");

        let mcaptcha = MCaptcha::new(
            sitekey,
            secret,
            Url::parse(&instance_url).expect("instance_url is not URL"),
        );
        assert!(mcaptcha.verify(&token).await.unwrap());
        assert!(!mcaptcha.verify("foo").await.unwrap());
    }
}