diff --git a/Cargo.lock b/Cargo.lock index 04236fb07936007e2f800ec52e4a1ac57ef8b6ad..35b6e3a747fa862cf311fabf4e8ec2c97e8aae63 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 207a9c84a1e493262fe9f1fea6fc370e272e5050..a9b2247f11bf66f1138ad9598312fa0fe125e72b 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 124dc5ba576f285456e5ef3bc027a669b2763e1b..f3f213ef564b2ab22c373e57220187dadd133752 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 8409ec9568b223ad7e0d5309baa0b5a17328d088..516741f719775ca0b38c09af2e351300cfbd3a81 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 269cb55eeb5c69ec2327c110193ee850a83061fa..29715ac9f16807363964f6fe1639aa84f9de1445 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 0000000000000000000000000000000000000000..a9f62137a98acb9e4c3e68b9dbe1fceb8d905079 --- /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 038b896d720124043efcec3763eafd2633297bd1..c88ae6378ce59c38c3008808efcab37b453f960a 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()) }