-
-
Notifications
You must be signed in to change notification settings - Fork 884
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
16 changed files
with
300 additions
and
20 deletions.
There are no files selected for viewing
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Oops, something went wrong.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,53 @@ | ||
use crate::{captcha_as_wav_base64, Perform}; | ||
use actix_web::web::Data; | ||
use captcha::{gen, Difficulty}; | ||
use chrono::Duration; | ||
use lemmy_api_common::{ | ||
context::LemmyContext, | ||
person::{CaptchaResponse, GetCaptcha, GetCaptchaResponse}, | ||
}; | ||
use lemmy_db_schema::{ | ||
source::{captcha_answer::CaptchaAnswer, local_site::LocalSite}, | ||
utils::naive_now, | ||
}; | ||
use lemmy_utils::error::LemmyError; | ||
|
||
#[async_trait::async_trait(?Send)] | ||
impl Perform for GetCaptcha { | ||
type Response = GetCaptchaResponse; | ||
|
||
#[tracing::instrument(skip(context))] | ||
async fn perform(&self, context: &Data<LemmyContext>) -> Result<Self::Response, LemmyError> { | ||
let local_site = LocalSite::read(context.pool()).await?; | ||
|
||
if !local_site.captcha_enabled { | ||
return Ok(GetCaptchaResponse { ok: None }); | ||
} | ||
|
||
let captcha = gen(match local_site.captcha_difficulty.as_str() { | ||
"easy" => Difficulty::Easy, | ||
"hard" => Difficulty::Hard, | ||
_ => Difficulty::Medium, | ||
}); | ||
|
||
let answer = captcha.chars_as_string(); | ||
|
||
let png = captcha.as_base64().expect("failed to generate captcha"); | ||
|
||
let uuid = uuid::Uuid::new_v4().to_string(); | ||
|
||
let wav = captcha_as_wav_base64(&captcha); | ||
|
||
let captcha: CaptchaAnswer = CaptchaAnswer { | ||
answer, | ||
uuid: uuid.clone(), | ||
expires: naive_now() + Duration::minutes(10), // expires in 10 minutes | ||
}; | ||
// Stores the captcha item in the db | ||
CaptchaAnswer::insert(context.pool(), &captcha).await?; | ||
|
||
Ok(GetCaptchaResponse { | ||
ok: Some(CaptchaResponse { png, wav, uuid }), | ||
}) | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,164 @@ | ||
use crate::{ | ||
schema::captcha_answer, | ||
source::captcha_answer::CaptchaAnswer, | ||
utils::{functions::lower, get_conn, naive_now, DbPool}, | ||
}; | ||
use diesel::{ | ||
delete, | ||
dsl::exists, | ||
insert_into, | ||
result::Error, | ||
select, | ||
ExpressionMethods, | ||
QueryDsl, | ||
}; | ||
use diesel_async::RunQueryDsl; | ||
|
||
impl CaptchaAnswer { | ||
pub async fn insert(pool: &DbPool, captcha: &CaptchaAnswer) -> Result<Self, Error> { | ||
let conn = &mut get_conn(pool).await?; | ||
|
||
insert_into(captcha_answer::table) | ||
.values(captcha) | ||
.get_result::<Self>(conn) | ||
.await | ||
} | ||
|
||
pub async fn check_captcha(pool: &DbPool, to_check: CaptchaAnswer) -> Result<bool, Error> { | ||
let conn = &mut get_conn(pool).await?; | ||
|
||
// delete any expired captchas | ||
delete(captcha_answer::table.filter(captcha_answer::expires.lt(&naive_now()))) | ||
.execute(conn) | ||
.await?; | ||
|
||
// fetch requested captcha | ||
let captcha_exists = select(exists( | ||
captcha_answer::dsl::captcha_answer | ||
.filter((captcha_answer::dsl::uuid).eq(to_check.uuid.clone())) | ||
.filter(lower(captcha_answer::dsl::answer).eq(to_check.answer.to_lowercase().clone())), | ||
)) | ||
.get_result::<bool>(conn) | ||
.await?; | ||
|
||
// delete checked captcha | ||
delete(captcha_answer::table.filter(captcha_answer::uuid.eq(to_check.uuid.clone()))) | ||
.execute(conn) | ||
.await?; | ||
|
||
Ok(captcha_exists) | ||
} | ||
} | ||
|
||
#[cfg(test)] | ||
mod tests { | ||
use crate::{ | ||
source::captcha_answer::CaptchaAnswer, | ||
utils::{build_db_pool_for_tests, naive_now}, | ||
}; | ||
use chrono::Duration; | ||
use serial_test::serial; | ||
|
||
#[tokio::test] | ||
#[serial] | ||
async fn test_captcha_happy_path() { | ||
let pool = &build_db_pool_for_tests().await; | ||
|
||
let captcha_a_id = "a".to_string(); | ||
|
||
let _ = CaptchaAnswer::insert( | ||
pool, | ||
&CaptchaAnswer { | ||
uuid: captcha_a_id.clone(), | ||
answer: "XYZ".to_string(), | ||
expires: naive_now() + Duration::minutes(10), | ||
}, | ||
) | ||
.await; | ||
|
||
let result = CaptchaAnswer::check_captcha( | ||
pool, | ||
CaptchaAnswer { | ||
uuid: captcha_a_id.clone(), | ||
answer: "xyz".to_string(), | ||
expires: chrono::NaiveDateTime::MIN, | ||
}, | ||
) | ||
.await; | ||
|
||
assert!(result.is_ok()); | ||
assert!(result.unwrap()); | ||
} | ||
|
||
#[tokio::test] | ||
#[serial] | ||
async fn test_captcha_repeat_answer_fails() { | ||
let pool = &build_db_pool_for_tests().await; | ||
|
||
let captcha_a_id = "a".to_string(); | ||
|
||
let _ = CaptchaAnswer::insert( | ||
pool, | ||
&CaptchaAnswer { | ||
uuid: captcha_a_id.clone(), | ||
answer: "XYZ".to_string(), | ||
expires: naive_now() + Duration::minutes(10), | ||
}, | ||
) | ||
.await; | ||
|
||
let _result = CaptchaAnswer::check_captcha( | ||
pool, | ||
CaptchaAnswer { | ||
uuid: captcha_a_id.clone(), | ||
answer: "xyz".to_string(), | ||
expires: chrono::NaiveDateTime::MIN, | ||
}, | ||
) | ||
.await; | ||
|
||
let result_repeat = CaptchaAnswer::check_captcha( | ||
pool, | ||
CaptchaAnswer { | ||
uuid: captcha_a_id.clone(), | ||
answer: "xyz".to_string(), | ||
expires: chrono::NaiveDateTime::MIN, | ||
}, | ||
) | ||
.await; | ||
|
||
assert!(result_repeat.is_ok()); | ||
assert!(!result_repeat.unwrap()); | ||
} | ||
|
||
#[tokio::test] | ||
#[serial] | ||
async fn test_captcha_expired_fails() { | ||
let pool = &build_db_pool_for_tests().await; | ||
|
||
let expired_id = "already_expired".to_string(); | ||
|
||
let _ = CaptchaAnswer::insert( | ||
pool, | ||
&CaptchaAnswer { | ||
uuid: expired_id.clone(), | ||
answer: "xyz".to_string(), | ||
expires: naive_now() - Duration::seconds(1), | ||
}, | ||
) | ||
.await; | ||
|
||
let expired_result = CaptchaAnswer::check_captcha( | ||
pool, | ||
CaptchaAnswer { | ||
uuid: expired_id.clone(), | ||
answer: "xyz".to_string(), | ||
expires: chrono::NaiveDateTime::MIN, | ||
}, | ||
) | ||
.await; | ||
|
||
assert!(expired_result.is_ok()); | ||
assert!(!expired_result.unwrap()); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.