From f44180a9809020a53bc6ef60b110b5a77f4b2202 Mon Sep 17 00:00:00 2001
From: Paul Makles <paulmakles@gmail.com>
Date: Thu, 13 Aug 2020 13:06:06 +0200
Subject: [PATCH] Add hCaptcha support.

---
 Cargo.lock            |  2 +-
 Cargo.toml            |  2 +-
 src/routes/account.rs | 22 ++++++++++++++++++++++
 src/routes/guild.rs   |  2 +-
 src/routes/root.rs    |  8 ++++++--
 src/util/captcha.rs   | 42 ++++++++++++++++++++++++++++++++++++++++++
 src/util/mod.rs       |  2 ++
 7 files changed, 75 insertions(+), 5 deletions(-)
 create mode 100644 src/util/captcha.rs

diff --git a/Cargo.lock b/Cargo.lock
index 04236fb..35b6e3a 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -1907,7 +1907,7 @@ dependencies = [
 
 [[package]]
 name = "revolt"
-version = "0.2.8"
+version = "0.2.9"
 dependencies = [
  "bcrypt",
  "bitfield",
diff --git a/Cargo.toml b/Cargo.toml
index 207a9c8..a9b2247 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -1,6 +1,6 @@
 [package]
 name = "revolt"
-version = "0.2.8"
+version = "0.2.9"
 authors = ["Paul Makles <paulmakles@gmail.com>"]
 edition = "2018"
 
diff --git a/src/routes/account.rs b/src/routes/account.rs
index 124dc5b..f3f213e 100644
--- a/src/routes/account.rs
+++ b/src/routes/account.rs
@@ -2,6 +2,7 @@ use super::Response;
 use crate::database;
 use crate::email;
 use crate::util::gen_token;
+use crate::util::captcha;
 
 use bcrypt::{hash, verify};
 use chrono::prelude::*;
@@ -17,6 +18,7 @@ pub struct Create {
     username: String,
     password: String,
     email: String,
+    captcha: Option<String>,
 }
 
 /// create a new Revolt account
@@ -28,6 +30,12 @@ pub struct Create {
 /// (3) add user and send email verification
 #[post("/create", data = "<info>")]
 pub fn create(info: Json<Create>) -> Response {
+    if let Err(error) = captcha::verify(&info.captcha) {
+        return Response::BadRequest(
+            json!({ "error": error })
+        );
+    }
+
     let col = database::get_collection("users");
 
     if info.username.len() < 2 || info.username.len() > 32 {
@@ -150,6 +158,7 @@ pub fn verify_email(code: String) -> Response {
 #[derive(Serialize, Deserialize)]
 pub struct Resend {
     email: String,
+    captcha: Option<String>,
 }
 
 /// resend a verification email
@@ -158,6 +167,12 @@ pub struct Resend {
 /// (3) resend the email
 #[post("/resend", data = "<info>")]
 pub fn resend_email(info: Json<Resend>) -> Response {
+    if let Err(error) = captcha::verify(&info.captcha) {
+        return Response::BadRequest(
+            json!({ "error": error })
+        );
+    }
+
     let col = database::get_collection("users");
 
     if let Some(u) = col
@@ -218,6 +233,7 @@ pub fn resend_email(info: Json<Resend>) -> Response {
 pub struct Login {
     email: String,
     password: String,
+    captcha: Option<String>,
 }
 
 /// login to a Revolt account
@@ -226,6 +242,12 @@ pub struct Login {
 /// (3) return access token
 #[post("/login", data = "<info>")]
 pub fn login(info: Json<Login>) -> Response {
+    if let Err(error) = captcha::verify(&info.captcha) {
+        return Response::BadRequest(
+            json!({ "error": error })
+        );
+    }
+
     let col = database::get_collection("users");
 
     if let Some(u) = col
diff --git a/src/routes/guild.rs b/src/routes/guild.rs
index 8409ec9..516741f 100644
--- a/src/routes/guild.rs
+++ b/src/routes/guild.rs
@@ -2,7 +2,7 @@ use super::channel::ChannelType;
 use super::Response;
 use crate::database::guild::{fetch_member as get_member, get_invite, Guild, MemberKey};
 use crate::database::{
-    self, channel::fetch_channel, guild::fetch_guilds, guild::serialise_guilds_with_channels, channel::Channel, Permission, PermissionCalculator, user::User
+    self, channel::fetch_channel, guild::serialise_guilds_with_channels, channel::Channel, Permission, PermissionCalculator, user::User
 };
 use crate::notifications::{
     self,
diff --git a/src/routes/root.rs b/src/routes/root.rs
index 269cb55..29715ac 100644
--- a/src/routes/root.rs
+++ b/src/routes/root.rs
@@ -1,16 +1,20 @@
 use super::Response;
 
 use mongodb::bson::doc;
+use std::env;
 
 /// root
 #[get("/")]
 pub fn root() -> Response {
     Response::Success(json!({
-        "revolt": "0.2.8",
+        "revolt": "0.2.9",
         "version": {
             "major": 0,
             "minor": 2,
-            "patch": 8
+            "patch": 9
+        },
+        "features": {
+            "captcha": env::var("HCAPTCHA_KEY").is_ok()
         }
     }))
 }
diff --git a/src/util/captcha.rs b/src/util/captcha.rs
new file mode 100644
index 0000000..a9f6213
--- /dev/null
+++ b/src/util/captcha.rs
@@ -0,0 +1,42 @@
+use serde::{Serialize, Deserialize};
+use reqwest::blocking::Client;
+use std::collections::HashMap;
+use std::env;
+
+#[derive(Serialize, Deserialize)]
+struct CaptchaResponse {
+    success: bool
+}
+
+pub fn verify(user_token: &Option<String>) -> Result<(), String> {
+    if let Ok(key) = env::var("HCAPTCHA_KEY") {
+        if let Some(token) = user_token {
+            let mut map = HashMap::new();
+            map.insert("secret", key);
+            map.insert("response", token.to_string());
+
+            let client = Client::new();
+            if let Ok(response) = client
+                .post("https://hcaptcha.com/siteverify")
+                .json(&map)
+                .send()
+            {
+                let result: CaptchaResponse = response
+                    .json()
+                    .map_err(|_| "Failed to deserialise captcha result.".to_string())?;
+
+                if result.success {
+                    Ok(())
+                } else {
+                    Err("Unsuccessful captcha verification".to_string())
+                }
+            } else {
+                Err("Failed to verify with hCaptcha".to_string())
+            }
+        } else {
+            Err("Missing hCaptcha token!".to_string())
+        }
+    } else {
+        Ok(())
+    }
+}
diff --git a/src/util/mod.rs b/src/util/mod.rs
index 038b896..c88ae63 100644
--- a/src/util/mod.rs
+++ b/src/util/mod.rs
@@ -2,6 +2,8 @@ use hashbrown::HashSet;
 use rand::{distributions::Alphanumeric, Rng};
 use std::iter::FromIterator;
 
+pub mod captcha;
+
 pub fn vec_to_set<T: Clone + Eq + std::hash::Hash>(data: &[T]) -> HashSet<T> {
     HashSet::from_iter(data.iter().cloned())
 }
-- 
GitLab