"base64 0.13.1",
"bcrypt",
"captcha",
- "chrono",
"lemmy_api_common",
"lemmy_db_schema",
"lemmy_db_views",
"actix-web",
"async-trait",
"bcrypt",
- "chrono",
"lemmy_api_common",
"lemmy_db_schema",
"lemmy_db_views",
captcha = { workspace = true }
anyhow = { workspace = true }
tracing = { workspace = true }
-chrono = { workspace = true }
[dev-dependencies]
serial_test = { workspace = true }
use actix_web::web::Data;
-use captcha::Captcha;
use lemmy_api_common::{context::LemmyContext, utils::local_site_to_slur_regex};
use lemmy_db_schema::source::local_site::LocalSite;
use lemmy_utils::{error::LemmyError, utils::slurs::check_slurs};
async fn perform(&self, context: &Data<LemmyContext>) -> Result<Self::Response, LemmyError>;
}
-/// Converts the captcha to a base64 encoded wav audio file
-pub(crate) fn captcha_as_wav_base64(captcha: &Captcha) -> String {
- let letters = captcha.as_wav();
-
- let mut concat_letters: Vec<u8> = Vec::new();
-
- for letter in letters {
- let bytes = letter.unwrap_or_default();
- concat_letters.extend(bytes);
- }
-
- // Convert to base64
- base64::encode(concat_letters)
-}
-
/// Check size of report and remove whitespace
pub(crate) fn check_report_reason(reason: &str, local_site: &LocalSite) -> Result<(), LemmyError> {
let slur_regex = &local_site_to_slur_regex(local_site);
+++ /dev/null
-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 }),
- })
- }
-}
mod block;
mod change_password;
mod change_password_after_reset;
-mod get_captcha;
mod list_banned;
mod login;
mod notifications;
url = { workspace = true }
async-trait = { workspace = true }
webmention = "0.4.0"
-chrono = { worspace = true }
use crate::PerformCrud;
use activitypub_federation::http_signatures::generate_actor_keypair;
use actix_web::web::Data;
-use chrono::NaiveDateTime;
use lemmy_api_common::{
context::LemmyContext,
person::{LoginResponse, Register},
use lemmy_db_schema::{
aggregates::structs::PersonAggregates,
source::{
- captcha_answer::CaptchaAnswer,
local_user::{LocalUser, LocalUserInsertForm},
person::{Person, PersonInsertForm},
registration_application::{RegistrationApplication, RegistrationApplicationInsertForm},
return Err(LemmyError::from_message("passwords_dont_match"));
}
- if local_site.site_setup && local_site.captcha_enabled {
- let check = CaptchaAnswer::check_captcha(
- context.pool(),
- CaptchaAnswer {
- uuid: data.captcha_uuid.clone().unwrap_or_default(),
- answer: data.captcha_answer.clone().unwrap_or_default(),
- // not used when checking
- expires: NaiveDateTime::MIN,
- },
- )
- .await?;
- if !check {
- return Err(LemmyError::from_message("captcha_incorrect"));
- }
- }
-
let slur_regex = local_site_to_slur_regex(&local_site);
check_slurs(&data.username, &slur_regex)?;
check_slurs_opt(&data.answer, &slur_regex)?;
---- schema.rs 2023-06-21 22:25:50.252384233 +0100
-+++ "schema copy.rs" 2023-06-21 22:26:50.452378651 +0100
-@@ -6,10 +6,6 @@
- pub struct ListingTypeEnum;
+diff --git a/crates/db_schema/src/schema.rs b/crates/db_schema/src/schema.rs
+index 255c6422..f2ccf5e2 100644
+--- a/crates/db_schema/src/schema.rs
++++ b/crates/db_schema/src/schema.rs
+@@ -2,16 +2,12 @@
+ pub mod sql_types {
#[derive(diesel::sql_types::SqlType)]
+ #[diesel(postgres_type(name = "listing_type_enum"))]
+ pub struct ListingTypeEnum;
+
+- #[derive(diesel::sql_types::SqlType)]
- #[diesel(postgres_type(name = "ltree"))]
- pub struct Ltree;
-
-- #[derive(diesel::sql_types::SqlType)]
+ #[derive(diesel::sql_types::SqlType)]
#[diesel(postgres_type(name = "registration_mode_enum"))]
pub struct RegistrationModeEnum;
-@@ -78,7 +74,7 @@
+ #[derive(diesel::sql_types::SqlType)]
+ #[diesel(postgres_type(name = "sort_type_enum"))]
+@@ -67,13 +63,13 @@ diesel::table! {
+ when_ -> Timestamp,
+ }
+ }
diesel::table! {
use diesel::sql_types::*;
comment (id) {
id -> Int4,
+ creator_id -> Int4,
+ post_id -> Int4,
+ content -> Text,
+++ /dev/null
-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());
- }
-}
pub mod activity;
pub mod actor_language;
-pub mod captcha_answer;
pub mod comment;
pub mod comment_reply;
pub mod comment_report;
}
}
-diesel::table! {
- captcha_answer (uuid) {
- uuid -> Text,
- answer -> Text,
- expires -> Timestamp,
- }
-}
-
diesel::table! {
use diesel::sql_types::{Bool, Int4, Nullable, Text, Timestamp, Varchar};
use diesel_ltree::sql_types::Ltree;
admin_purge_community,
admin_purge_person,
admin_purge_post,
- captcha_answer,
comment,
comment_aggregates,
comment_like,
+++ /dev/null
-#[cfg(feature = "full")]
-use crate::schema::captcha_answer;
-use serde::{Deserialize, Serialize};
-use serde_with::skip_serializing_none;
-
-#[skip_serializing_none]
-#[derive(Clone, PartialEq, Eq, Debug, Serialize, Deserialize)]
-#[cfg_attr(feature = "full", derive(Queryable, Insertable, AsChangeset))]
-#[cfg_attr(feature = "full", diesel(table_name = captcha_answer))]
-pub struct CaptchaAnswer {
- pub uuid: String,
- pub answer: String,
- pub expires: chrono::NaiveDateTime,
-}
#[cfg(feature = "full")]
pub mod activity;
pub mod actor_language;
-pub mod captcha_answer;
pub mod comment;
pub mod comment_reply;
pub mod comment_report;
+++ /dev/null
-drop table captcha_answer;
\ No newline at end of file
+++ /dev/null
-create table captcha_answer (
- uuid text not null primary key,
- answer text not null,
- expires timestamp not null
-);
ChangePassword,
DeleteAccount,
GetBannedPersons,
- GetCaptcha,
GetPersonDetails,
GetPersonMentions,
GetReplies,
.wrap(rate_limit.register())
.route(web::post().to(route_post_crud::<Register>)),
)
- .service(
- // Handle captcha separately
- web::resource("/user/get_captcha")
- .wrap(rate_limit.post())
- .route(web::get().to(route_get::<GetCaptcha>)),
- )
// User actions
.service(
web::scope("/user")