Adding visual captchas for register and login.

This commit is contained in:
Dessalines 2020-07-26 00:29:44 -04:00
parent 617d636432
commit cde76af4aa
16 changed files with 539 additions and 55 deletions

View file

@ -360,6 +360,8 @@ The `jwt` string should be stored and used anywhere `auth` is called for.
data: {
username_or_email: String,
password: String
captcha_uuid: String,
captcha_answer: String,
}
}
```
@ -390,7 +392,9 @@ Only the first user will be able to be the admin.
email: Option<String>,
password: String,
password_verify: String,
admin: bool
admin: bool,
captcha_uuid: String,
captcha_answer: String,
}
}
```
@ -408,6 +412,31 @@ Only the first user will be able to be the admin.
`POST /user/register`
#### Get Captcha
These expire after 10 minutes.
##### Request
```rust
{
op: "GetCaptcha",
}
```
##### Response
```rust
{
op: "GetCaptcha",
data: {
png: String, // A Base64 encoded png
uuid: String,
}
}
```
##### HTTP
`GET /user/get_captcha`
#### Get User Details
##### Request
```rust

295
server/Cargo.lock generated vendored
View file

@ -31,7 +31,7 @@ checksum = "a9028932f36d45df020c92317ccb879ab77d8f066f57ff143dd5bee93ba3de0d"
dependencies = [
"actix-rt",
"actix_derive",
"bitflags",
"bitflags 1.2.1",
"bytes",
"crossbeam-channel",
"derive_more",
@ -54,7 +54,7 @@ version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "09e55f0a5c2ca15795035d90c46bd0e73a5123b72f68f12596d6ba5282051380"
dependencies = [
"bitflags",
"bitflags 1.2.1",
"bytes",
"futures-core",
"futures-sink",
@ -94,7 +94,7 @@ dependencies = [
"actix-http",
"actix-service",
"actix-web",
"bitflags",
"bitflags 1.2.1",
"bytes",
"derive_more",
"futures-core",
@ -120,7 +120,7 @@ dependencies = [
"actix-tls",
"actix-utils",
"base64 0.12.3",
"bitflags",
"bitflags 1.2.1",
"brotli2",
"bytes",
"cookie",
@ -280,7 +280,7 @@ dependencies = [
"actix-codec",
"actix-rt",
"actix-service",
"bitflags",
"bitflags 1.2.1",
"bytes",
"either",
"futures",
@ -382,6 +382,12 @@ version = "0.2.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ee2a4ec343196209d6594e19543ae87a39f96d5534d7174822a3ad825dd6ed7e"
[[package]]
name = "adler32"
version = "1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "567b077b825e468cc974f0020d4082ee6e03132512f207ef1a02fd5d00d1f32d"
[[package]]
name = "aho-corasick"
version = "0.7.13"
@ -496,6 +502,15 @@ version = "0.2.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1b20b618342cf9891c292c4f5ac2cde7287cc5c87e87e9c769d617793607dec1"
[[package]]
name = "base64"
version = "0.5.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "30e93c03064e7590d0466209155251b90c22e37fab1daf2771582598b5827557"
dependencies = [
"byteorder",
]
[[package]]
name = "base64"
version = "0.9.3"
@ -539,6 +554,12 @@ dependencies = [
"getrandom",
]
[[package]]
name = "bitflags"
version = "0.7.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "aad18937a628ec6abcd26d1489012cc0e18c21798210f491af69ded9b881106d"
[[package]]
name = "bitflags"
version = "1.2.1"
@ -642,6 +663,12 @@ version = "0.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e3b5ca7a04898ad4bcd41c90c5285445ff5b791899bb1b0abdd2a2aa791211d7"
[[package]]
name = "bytemuck"
version = "1.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "db7a1029718df60331e557c9e83a55523c955e5dd2a7bfeffad6bbd50b538ae9"
[[package]]
name = "byteorder"
version = "1.3.4"
@ -663,6 +690,26 @@ dependencies = [
"bytes",
]
[[package]]
name = "c_vec"
version = "1.3.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f8a318911dce53b5f1ca6539c44f5342c632269f0fa7ea3e35f32458c27a7c30"
[[package]]
name = "captcha"
version = "0.0.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4d060a3be43adb2fe89d3448e9a193149806139b1ce99281865fcab7aeaf04ed"
dependencies = [
"base64 0.5.2",
"image",
"lodepng",
"rand 0.3.23",
"serde_json",
"time 0.1.43",
]
[[package]]
name = "cc"
version = "1.0.58"
@ -695,7 +742,7 @@ checksum = "bdfa80d47f954d53a35a64987ca1422f495b8d6483c0fe9f7117b36c2a792129"
dependencies = [
"ansi_term",
"atty",
"bitflags",
"bitflags 1.2.1",
"strsim 0.8.0",
"textwrap",
"unicode-width",
@ -708,7 +755,7 @@ version = "0.0.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ddfc5b9aa5d4507acaf872de71051dfd0e309860e88966e1051e462a077aac4f"
dependencies = [
"bitflags",
"bitflags 1.2.1",
]
[[package]]
@ -717,9 +764,15 @@ version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4344512281c643ae7638bbabc3af17a11307803ec8f0fcad9fae512a8bf36467"
dependencies = [
"bitflags",
"bitflags 1.2.1",
]
[[package]]
name = "color_quant"
version = "1.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0dbbb57365263e881e805dc77d94697c9118fd94d8da011240555aa7b23445bd"
[[package]]
name = "comrak"
version = "0.7.0"
@ -806,6 +859,43 @@ dependencies = [
"crossbeam-utils",
]
[[package]]
name = "crossbeam-deque"
version = "0.7.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9f02af974daeee82218205558e51ec8768b48cf524bd01d550abe5573a608285"
dependencies = [
"crossbeam-epoch",
"crossbeam-utils",
"maybe-uninit",
]
[[package]]
name = "crossbeam-epoch"
version = "0.8.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "058ed274caafc1f60c4997b5fc07bf7dc7cca454af7c6e81edffe5f33f70dace"
dependencies = [
"autocfg 1.0.0",
"cfg-if",
"crossbeam-utils",
"lazy_static",
"maybe-uninit",
"memoffset",
"scopeguard",
]
[[package]]
name = "crossbeam-queue"
version = "0.2.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "774ba60a54c213d409d5353bda12d49cd68d14e45036a285234c8d6f91f92570"
dependencies = [
"cfg-if",
"crossbeam-utils",
"maybe-uninit",
]
[[package]]
name = "crossbeam-utils"
version = "0.7.2"
@ -852,6 +942,16 @@ dependencies = [
"syn",
]
[[package]]
name = "deflate"
version = "0.7.20"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "707b6a7b384888a70c8d2e8650b3e60170dfc6a67bb4aa67b6dfca57af4bedb4"
dependencies = [
"adler32",
"byteorder",
]
[[package]]
name = "derive_builder"
version = "0.9.0"
@ -894,7 +994,7 @@ version = "1.4.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3e2de9deab977a153492a1468d1b1c0662c1cf39e5ea87d0c060ecd59ef18d8c"
dependencies = [
"bitflags",
"bitflags 1.2.1",
"byteorder",
"chrono",
"diesel_derives",
@ -1072,6 +1172,15 @@ dependencies = [
"syn",
]
[[package]]
name = "enum_primitive"
version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "be4551092f4d519593039259a9ed8daedf0da12e5109c5280338073eaeb81180"
dependencies = [
"num-traits 0.1.43",
]
[[package]]
name = "env_logger"
version = "0.7.1"
@ -1167,7 +1276,7 @@ version = "0.3.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2e9763c69ebaae630ba35f74888db465e49e259ba1bc0eda7d06f4a067615d82"
dependencies = [
"bitflags",
"bitflags 1.2.1",
"fuchsia-zircon-sys",
]
@ -1311,6 +1420,16 @@ dependencies = [
"wasi",
]
[[package]]
name = "gif"
version = "0.9.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e2e41945ba23db3bf51b24756d73d81acb4f28d85c3dccc32c6fae904438c25f"
dependencies = [
"color_quant",
"lzw",
]
[[package]]
name = "gimli"
version = "0.22.0"
@ -1455,6 +1574,23 @@ dependencies = [
"unicode-normalization",
]
[[package]]
name = "image"
version = "0.13.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1c3f4f5ea213ed9899eca760a8a14091d4b82d33e27cf8ced336ff730e9f6da8"
dependencies = [
"byteorder",
"enum_primitive",
"gif",
"jpeg-decoder",
"num-iter",
"num-rational",
"num-traits 0.1.43",
"png",
"scoped_threadpool",
]
[[package]]
name = "indexmap"
version = "1.5.0"
@ -1465,6 +1601,12 @@ dependencies = [
"hashbrown",
]
[[package]]
name = "inflate"
version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d1238524675af3938a7c74980899535854b88ba07907bb1c944abe5b8fc437e5"
[[package]]
name = "instant"
version = "0.1.6"
@ -1507,6 +1649,16 @@ version = "0.4.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dc6f3ad7b9d11a0c00842ff8de1b60ee58661048eb8049ed33c73594f359d7e6"
[[package]]
name = "jpeg-decoder"
version = "0.1.20"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cc797adac5f083b8ff0ca6f6294a999393d76e197c36488e2ef732c4715f6fa3"
dependencies = [
"byteorder",
"rayon",
]
[[package]]
name = "js-sys"
version = "0.3.42"
@ -1585,6 +1737,7 @@ dependencies = [
"awc",
"base64 0.12.3",
"bcrypt",
"captcha",
"chrono",
"diesel",
"diesel_migrations",
@ -1673,7 +1826,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "db65c6da02e61f55dae90a0ae427b2a5f6b3e8db09f58d10efab23af92592616"
dependencies = [
"arrayvec",
"bitflags",
"bitflags 1.2.1",
"cfg-if",
"ryu",
"static_assertions",
@ -1719,6 +1872,18 @@ dependencies = [
"scopeguard",
]
[[package]]
name = "lodepng"
version = "1.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8ac1dfdf85b7d5dea61a620e12c051a72078189366a0b3c0ab331e30847def2f"
dependencies = [
"c_vec",
"cc",
"libc",
"rgb",
]
[[package]]
name = "log"
version = "0.4.11"
@ -1737,6 +1902,12 @@ dependencies = [
"linked-hash-map 0.5.3",
]
[[package]]
name = "lzw"
version = "0.10.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7d947cbb889ed21c2a84be6ffbaebf5b4e0f4340638cba0444907e38b56be084"
[[package]]
name = "maplit"
version = "1.0.2"
@ -1755,12 +1926,27 @@ version = "0.1.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7ffc5c5338469d4d3ea17d269fa8ea3512ad247247c30bd2df69e68309ed0a08"
[[package]]
name = "maybe-uninit"
version = "2.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "60302e4db3a61da70c0cb7991976248362f30319e88850c487b9b95bbf059e00"
[[package]]
name = "memchr"
version = "2.3.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3728d817d99e5ac407411fa471ff9800a778d88a24685968b36824eaf4bee400"
[[package]]
name = "memoffset"
version = "0.5.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c198b026e1bbf08a937e94c6c60f9ec4a2267f5b0d2eec9c1b21b061ce2be55f"
dependencies = [
"autocfg 1.0.0",
]
[[package]]
name = "migrations_internals"
version = "1.4.1"
@ -1920,6 +2106,27 @@ dependencies = [
"num-traits 0.2.12",
]
[[package]]
name = "num-iter"
version = "0.1.41"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7a6e6b7c748f995c4c29c5f5ae0248536e04a5739927c74ec0fa564805094b9f"
dependencies = [
"autocfg 1.0.0",
"num-integer",
"num-traits 0.2.12",
]
[[package]]
name = "num-rational"
version = "0.1.42"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ee314c74bd753fc86b4780aa9475da469155f3848473a261d2d18e35245a784e"
dependencies = [
"num-integer",
"num-traits 0.2.12",
]
[[package]]
name = "num-traits"
version = "0.1.43"
@ -1978,7 +2185,7 @@ version = "0.10.30"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8d575eff3665419f9b83678ff2815858ad9d11567e082f5ac1814baba4e2bcb4"
dependencies = [
"bitflags",
"bitflags 1.2.1",
"cfg-if",
"foreign-types",
"lazy_static",
@ -2153,6 +2360,18 @@ version = "0.3.18"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d36492546b6af1463394d46f0c834346f31548646f6ba10849802c9c9a27ac33"
[[package]]
name = "png"
version = "0.7.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "48f397b84083c2753ba53c7b56ad023edb94512b2885ffe227c66ff7edb61868"
dependencies = [
"bitflags 0.7.0",
"deflate",
"inflate",
"num-iter",
]
[[package]]
name = "ppv-lite86"
version = "0.2.8"
@ -2225,6 +2444,16 @@ dependencies = [
"scheduled-thread-pool",
]
[[package]]
name = "rand"
version = "0.3.23"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "64ac302d8f83c0c1974bf758f6b041c6c8ada916fbb44a609158ca8b064cc76c"
dependencies = [
"libc",
"rand 0.4.6",
]
[[package]]
name = "rand"
version = "0.4.6"
@ -2385,6 +2614,31 @@ dependencies = [
"rand_core 0.3.1",
]
[[package]]
name = "rayon"
version = "1.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "62f02856753d04e03e26929f820d0a0a337ebe71f849801eea335d464b349080"
dependencies = [
"autocfg 1.0.0",
"crossbeam-deque",
"either",
"rayon-core",
]
[[package]]
name = "rayon-core"
version = "1.7.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e92e15d89083484e11353891f1af602cc661426deb9564c298b270c726973280"
dependencies = [
"crossbeam-deque",
"crossbeam-queue",
"crossbeam-utils",
"lazy_static",
"num_cpus",
]
[[package]]
name = "rdrand"
version = "0.4.0"
@ -2437,6 +2691,15 @@ dependencies = [
"quick-error",
]
[[package]]
name = "rgb"
version = "0.8.20"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "90ef54b45ae131327a88597e2463fee4098ad6c88ba7b6af4b3987db8aad4098"
dependencies = [
"bytemuck",
]
[[package]]
name = "ring"
version = "0.16.15"
@ -2521,6 +2784,12 @@ dependencies = [
"parking_lot 0.11.0",
]
[[package]]
name = "scoped_threadpool"
version = "0.1.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1d51f5df5af43ab3f1360b429fa5e0152ac5ce8c0bd6485cae490332e96846a8"
[[package]]
name = "scopeguard"
version = "1.1.0"
@ -2543,7 +2812,7 @@ version = "0.4.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "64808902d7d99f78eaddd2b4e2509713babc3dc3c85ad6f4c447680f3c01e535"
dependencies = [
"bitflags",
"bitflags 1.2.1",
"core-foundation",
"core-foundation-sys",
"libc",

1
server/Cargo.toml vendored
View file

@ -51,3 +51,4 @@ itertools = "0.9.0"
uuid = { version = "0.8", features = ["serde", "v4"] }
sha2 = "0.9"
async-trait = "0.1.36"
captcha = "0.0.7"

View file

@ -133,10 +133,7 @@ impl Community {
.get_result::<Self>(conn)
}
fn community_mods_and_admins(
conn: &PgConnection,
community_id: i32,
) -> Result<Vec<i32>, Error> {
fn community_mods_and_admins(conn: &PgConnection, community_id: i32) -> Result<Vec<i32>, Error> {
use crate::{community_view::CommunityModeratorView, user_view::UserView};
let mut mods_and_admins: Vec<i32> = Vec::new();
mods_and_admins.append(

View file

@ -366,6 +366,8 @@ impl Perform for Oper<GetSite> {
password_verify: setup.admin_password.to_owned(),
admin: true,
show_nsfw: true,
captcha_uuid: "".to_string(),
captcha_answer: "".to_string(),
};
let login_response = Oper::new(register, self.client.clone())
.perform(pool, websocket_info.clone())

View file

@ -3,7 +3,7 @@ use crate::{
apub::ApubObjectType,
blocking,
websocket::{
server::{JoinUserRoom, SendAllMessage, SendUserRoomMessage},
server::{CaptchaItem, CheckCaptcha, JoinUserRoom, SendAllMessage, SendUserRoomMessage},
UserOperation,
WebsocketInfo,
},
@ -11,6 +11,8 @@ use crate::{
LemmyError,
};
use bcrypt::verify;
use captcha::{gen, Difficulty};
use chrono::Duration;
use lemmy_db::{
comment::*,
comment_view::*,
@ -56,6 +58,8 @@ use std::str::FromStr;
pub struct Login {
username_or_email: String,
password: String,
captcha_uuid: String,
captcha_answer: String,
}
#[derive(Serialize, Deserialize)]
@ -66,6 +70,17 @@ pub struct Register {
pub password_verify: String,
pub admin: bool,
pub show_nsfw: bool,
pub captcha_uuid: String,
pub captcha_answer: String,
}
#[derive(Serialize, Deserialize)]
pub struct GetCaptcha {}
#[derive(Serialize, Deserialize)]
pub struct GetCaptchaResponse {
png: String, // A Base64 encoded png
uuid: String,
}
#[derive(Serialize, Deserialize)]
@ -268,7 +283,7 @@ impl Perform for Oper<Login> {
async fn perform(
&self,
pool: &DbPool,
_websocket_info: Option<WebsocketInfo>,
websocket_info: Option<WebsocketInfo>,
) -> Result<LoginResponse, LemmyError> {
let data: &Login = &self.data;
@ -289,6 +304,27 @@ impl Perform for Oper<Login> {
return Err(APIError::err("password_incorrect").into());
}
// Verify the captcha
// Do an admin check, so that for your federation tests,
// you don't need to solve the captchas
if !user.admin {
match websocket_info {
Some(ws) => {
let check = ws
.chatserver
.send(CheckCaptcha {
uuid: data.captcha_uuid.to_owned(),
answer: data.captcha_answer.to_owned(),
})
.await?;
if !check {
return Err(APIError::err("captcha_incorrect").into());
}
}
None => return Err(APIError::err("captcha_incorrect").into()),
};
}
// Return the jwt
Ok(LoginResponse {
jwt: Claims::jwt(user, Settings::get().hostname),
@ -303,7 +339,7 @@ impl Perform for Oper<Register> {
async fn perform(
&self,
pool: &DbPool,
_websocket_info: Option<WebsocketInfo>,
websocket_info: Option<WebsocketInfo>,
) -> Result<LoginResponse, LemmyError> {
let data: &Register = &self.data;
@ -320,6 +356,25 @@ impl Perform for Oper<Register> {
return Err(APIError::err("passwords_dont_match").into());
}
// If its not the admin, check the captcha
if !data.admin {
match websocket_info {
Some(ws) => {
let check = ws
.chatserver
.send(CheckCaptcha {
uuid: data.captcha_uuid.to_owned(),
answer: data.captcha_answer.to_owned(),
})
.await?;
if !check {
return Err(APIError::err("captcha_incorrect").into());
}
}
None => return Err(APIError::err("captcha_incorrect").into()),
};
}
if let Err(slurs) = slur_check(&data.username) {
return Err(APIError::err(&slurs_vec_to_str(slurs)).into());
}
@ -439,6 +494,39 @@ impl Perform for Oper<Register> {
}
}
#[async_trait::async_trait(?Send)]
impl Perform for Oper<GetCaptcha> {
type Response = GetCaptchaResponse;
async fn perform(
&self,
_pool: &DbPool,
websocket_info: Option<WebsocketInfo>,
) -> Result<Self::Response, LemmyError> {
let captcha = gen(Difficulty::Medium);
let answer = captcha.chars_as_string();
let png_byte_array = captcha.as_png().expect("failed to generate captcha");
let png = base64::encode(png_byte_array);
let uuid = uuid::Uuid::new_v4().to_string();
let captcha_item = CaptchaItem {
answer,
uuid: uuid.to_owned(),
expires: naive_now() + Duration::minutes(10), // expires in 10 minutes
};
if let Some(ws) = websocket_info {
ws.chatserver.do_send(captcha_item);
}
Ok(GetCaptchaResponse { png, uuid })
}
}
#[async_trait::async_trait(?Send)]
impl Perform for Oper<SaveUserSettings> {
type Response = LoginResponse;

View file

@ -7,7 +7,9 @@ pub extern crate lazy_static;
pub extern crate failure;
pub extern crate actix;
pub extern crate actix_web;
pub extern crate base64;
pub extern crate bcrypt;
pub extern crate captcha;
pub extern crate chrono;
pub extern crate diesel;
pub extern crate dotenv;

View file

@ -140,6 +140,7 @@ pub fn config(cfg: &mut web::ServiceConfig, rate_limit: &RateLimit) {
.route("/ban", web::post().to(route_post::<BanUser>))
// Account actions. I don't like that they're in /user maybe /accounts
.route("/login", web::post().to(route_post::<Login>))
.route("/get_captcha", web::get().to(route_post::<GetCaptcha>))
.route(
"/delete_account",
web::post().to(route_post::<DeleteAccount>),

View file

@ -20,6 +20,7 @@ use std::{
pub enum UserOperation {
Login,
Register,
GetCaptcha,
CreateCommunity,
CreatePost,
ListCommunities,

View file

@ -16,6 +16,7 @@ use crate::{
UserId,
};
use actix_web::client::Client;
use lemmy_db::naive_now;
/// Chat server sends this messages to session
#[derive(Message)]
@ -134,6 +135,21 @@ pub struct SessionInfo {
pub ip: IPAddr,
}
#[derive(Message, Debug)]
#[rtype(result = "()")]
pub struct CaptchaItem {
pub uuid: String,
pub answer: String,
pub expires: chrono::NaiveDateTime,
}
#[derive(Message)]
#[rtype(bool)]
pub struct CheckCaptcha {
pub uuid: String,
pub answer: String,
}
/// `ChatServer` manages chat rooms and responsible for coordinating chat
/// session.
pub struct ChatServer {
@ -158,6 +174,9 @@ pub struct ChatServer {
/// Rate limiting based on rate type and IP addr
rate_limiter: RateLimit,
/// A list of the current captchas
captchas: Vec<CaptchaItem>,
/// An HTTP Client
client: Client,
}
@ -176,6 +195,7 @@ impl ChatServer {
rng: rand::thread_rng(),
pool,
rate_limiter,
captchas: Vec::new(),
client,
}
}
@ -441,6 +461,7 @@ impl ChatServer {
// User ops
UserOperation::Login => do_user_operation::<Login>(args).await,
UserOperation::Register => do_user_operation::<Register>(args).await,
UserOperation::GetCaptcha => do_user_operation::<GetCaptcha>(args).await,
UserOperation::GetUserDetails => do_user_operation::<GetUserDetails>(args).await,
UserOperation::GetReplies => do_user_operation::<GetReplies>(args).await,
UserOperation::AddAdmin => do_user_operation::<AddAdmin>(args).await,
@ -774,3 +795,25 @@ where
};
Ok(serde_json::to_string(&response)?)
}
impl Handler<CaptchaItem> for ChatServer {
type Result = ();
fn handle(&mut self, msg: CaptchaItem, _: &mut Context<Self>) {
self.captchas.push(msg);
}
}
impl Handler<CheckCaptcha> for ChatServer {
type Result = bool;
fn handle(&mut self, msg: CheckCaptcha, _: &mut Context<Self>) -> Self::Result {
// Remove all the ones that are past the expire time
self.captchas.retain(|x| x.expires.gt(&naive_now()));
self
.captchas
.iter()
.any(|r| r.uuid == msg.uuid && r.answer == msg.answer)
}
}

View file

@ -51,6 +51,8 @@ describe('main', () => {
let form: LoginForm = {
username_or_email: 'lemmy_alpha',
password: 'lemmy',
captcha_uuid: '', // Admins don't need this
captcha_answer: '',
};
let res: LoginResponse = await fetch(`${lemmyAlphaApiUrl}/user/login`, {
@ -67,6 +69,8 @@ describe('main', () => {
let formB = {
username_or_email: 'lemmy_beta',
password: 'lemmy',
captcha_uuid: '', // Admins don't need this
captcha_answer: '',
};
let resB: LoginResponse = await fetch(`${lemmyBetaApiUrl}/user/login`, {
@ -83,6 +87,8 @@ describe('main', () => {
let formC = {
username_or_email: 'lemmy_gamma',
password: 'lemmy',
captcha_uuid: '', // Admins don't need this
captcha_answer: '',
};
let resG: LoginResponse = await fetch(`${lemmyGammaApiUrl}/user/login`, {

View file

@ -8,6 +8,7 @@ import {
UserOperation,
PasswordResetForm,
GetSiteResponse,
GetCaptchaResponse,
WebSocketJsonResponse,
} from '../interfaces';
import { WebSocketService, UserService } from '../services';
@ -20,11 +21,7 @@ interface State {
loginLoading: boolean;
registerLoading: boolean;
enable_nsfw: boolean;
mathQuestion: {
a: number;
b: number;
answer: number;
};
captcha: GetCaptchaResponse;
}
export class Login extends Component<any, State> {
@ -34,6 +31,8 @@ export class Login extends Component<any, State> {
loginForm: {
username_or_email: undefined,
password: undefined,
captcha_uuid: undefined,
captcha_answer: undefined,
},
registerForm: {
username: undefined,
@ -41,14 +40,15 @@ export class Login extends Component<any, State> {
password_verify: undefined,
admin: false,
show_nsfw: false,
captcha_uuid: undefined,
captcha_answer: undefined,
},
loginLoading: false,
registerLoading: false,
enable_nsfw: undefined,
mathQuestion: {
a: Math.floor(Math.random() * 10) + 1,
b: Math.floor(Math.random() * 10) + 1,
answer: undefined,
captcha: {
png: undefined,
uuid: undefined,
},
};
@ -66,6 +66,7 @@ export class Login extends Component<any, State> {
);
WebSocketService.Instance.getSite();
WebSocketService.Instance.getCaptcha();
}
componentWillUnmount() {
@ -131,6 +132,26 @@ export class Login extends Component<any, State> {
)}
</div>
</div>
<div class="form-group row">
<label class="col-sm-2" htmlFor="login-captcha">
{i18n.t('enter_code')}
</label>
<div class="col-sm-4">
{this.state.captcha.uuid && (
<img class="rounded img-fluid" src={this.captchaSrc()} />
)}
</div>
<div class="col-sm-6">
<input
type="text"
class="form-control"
id="login-captcha"
value={this.state.loginForm.captcha_answer}
onInput={linkEvent(this, this.handleLoginCaptchaAnswerChange)}
required
/>
</div>
</div>
<div class="form-group row">
<div class="col-sm-10">
<button type="submit" class="btn btn-secondary">
@ -235,18 +256,21 @@ export class Login extends Component<any, State> {
</div>
</div>
<div class="form-group row">
<label class="col-sm-10 col-form-label" htmlFor="register-math">
{i18n.t('what_is')}{' '}
{`${this.state.mathQuestion.a} + ${this.state.mathQuestion.b}?`}
<label class="col-sm-2" htmlFor="register-captcha">
{i18n.t('enter_code')}
</label>
<div class="col-sm-2">
<div class="col-sm-4">
{this.state.captcha.uuid && (
<img class="rounded img-fluid" src={this.captchaSrc()} />
)}
</div>
<div class="col-sm-6">
<input
type="number"
id="register-math"
type="text"
class="form-control"
value={this.state.mathQuestion.answer}
onInput={linkEvent(this, this.handleMathAnswerChange)}
id="register-captcha"
value={this.state.registerForm.captcha_answer}
onInput={linkEvent(this, this.handleRegisterCaptchaAnswerChange)}
required
/>
</div>
@ -271,11 +295,7 @@ export class Login extends Component<any, State> {
)}
<div class="form-group row">
<div class="col-sm-10">
<button
type="submit"
class="btn btn-secondary"
disabled={this.mathCheck}
>
<button type="submit" class="btn btn-secondary">
{this.state.registerLoading ? (
<svg class="icon icon-spinner spin">
<use xlinkHref="#icon-spinner"></use>
@ -311,10 +331,7 @@ export class Login extends Component<any, State> {
event.preventDefault();
i.state.registerLoading = true;
i.setState(i.state);
if (!i.mathCheck) {
WebSocketService.Instance.register(i.state.registerForm);
}
WebSocketService.Instance.register(i.state.registerForm);
}
handleRegisterUsernameChange(i: Login, event: any) {
@ -345,8 +362,13 @@ export class Login extends Component<any, State> {
i.setState(i.state);
}
handleMathAnswerChange(i: Login, event: any) {
i.state.mathQuestion.answer = event.target.value;
handleLoginCaptchaAnswerChange(i: Login, event: any) {
i.state.loginForm.captcha_answer = event.target.value;
i.setState(i.state);
}
handleRegisterCaptchaAnswerChange(i: Login, event: any) {
i.state.registerForm.captcha_answer = event.target.value;
i.setState(i.state);
}
@ -358,11 +380,8 @@ export class Login extends Component<any, State> {
WebSocketService.Instance.passwordReset(resetForm);
}
get mathCheck(): boolean {
return (
this.state.mathQuestion.answer !=
this.state.mathQuestion.a + this.state.mathQuestion.b
);
captchaSrc() {
return `data:image/png;base64,${this.state.captcha.png}`;
}
parseMessage(msg: WebSocketJsonResponse) {
@ -388,6 +407,12 @@ export class Login extends Component<any, State> {
UserService.Instance.login(data);
WebSocketService.Instance.userJoin();
this.props.history.push('/communities');
} else if (res.op == UserOperation.GetCaptcha) {
let data = res.data as GetCaptchaResponse;
this.state.captcha = data;
this.state.loginForm.captcha_uuid = data.uuid;
this.state.registerForm.captcha_uuid = data.uuid;
this.setState({ captcha: data });
} else if (res.op == UserOperation.PasswordReset) {
toast(i18n.t('reset_password_mail_sent'));
} else if (res.op == UserOperation.GetSite) {

View file

@ -28,6 +28,9 @@ export class Setup extends Component<any, State> {
password_verify: undefined,
admin: true,
show_nsfw: true,
// The first admin signup doesn't need a captcha
captcha_uuid: '',
captcha_answer: '',
},
doneRegisteringUser: false,
userLoading: false,

11
ui/src/interfaces.ts vendored
View file

@ -1,6 +1,7 @@
export enum UserOperation {
Login,
Register,
GetCaptcha,
CreateCommunity,
CreatePost,
ListCommunities,
@ -548,6 +549,8 @@ export interface ModAdd {
export interface LoginForm {
username_or_email: string;
password: string;
captcha_uuid: string;
captcha_answer: string;
}
export interface RegisterForm {
@ -557,6 +560,13 @@ export interface RegisterForm {
password_verify: string;
admin: boolean;
show_nsfw: boolean;
captcha_uuid: string;
captcha_answer: string;
}
export interface GetCaptchaResponse {
png: string;
uuid: string;
}
export interface LoginResponse {
@ -990,6 +1000,7 @@ type ResponseType =
| CommentResponse
| UserMentionResponse
| LoginResponse
| GetCaptchaResponse
| GetModlogResponse
| SearchResponse
| BanFromCommunityResponse

View file

@ -114,6 +114,10 @@ export class WebSocketService {
this.ws.send(this.wsSendWrapper(UserOperation.Register, registerForm));
}
public getCaptcha() {
this.ws.send(this.wsSendWrapper(UserOperation.GetCaptcha, {}));
}
public createCommunity(form: CommunityForm) {
this.setAuth(form);
this.ws.send(this.wsSendWrapper(UserOperation.CreateCommunity, form));

View file

@ -264,6 +264,8 @@
"password_incorrect": "Password incorrect.",
"passwords_dont_match": "Passwords do not match.",
"no_password_reset": "You will not be able to reset your password without an email.",
"captcha_incorrect": "Captcha incorrect.",
"enter_code": "Enter Code",
"invalid_username": "Invalid username.",
"admin_already_created": "Sorry, there's already an admin.",
"user_already_exists": "User already exists.",