From b008e11fb046d7356712db4aef31f9b7be4915d1 Mon Sep 17 00:00:00 2001 From: Bat Date: Fri, 29 Jun 2018 14:22:43 +0200 Subject: [PATCH 1/7] Add validator --- Cargo.lock | 58 +++++++++++++++++++++++++++++++++++++++++++++++++++++ Cargo.toml | 10 +++++---- src/main.rs | 3 +++ 3 files changed, 67 insertions(+), 4 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index f20bc426..3e41543e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -592,6 +592,11 @@ dependencies = [ "unicode-normalization 0.1.5 (registry+https://github.com/rust-lang/crates.io-index)", ] +[[package]] +name = "if_chain" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" + [[package]] name = "indexmap" version = "1.0.1" @@ -995,6 +1000,8 @@ dependencies = [ "rocket_i18n 0.1.1 (git+https://github.com/BaptisteGelez/rocket_i18n?rev=5b4225d5bed5769482dc926a7e6d6b79f1217be6)", "rpassword 2.0.0 (registry+https://github.com/rust-lang/crates.io-index)", "serde_json 1.0.16 (registry+https://github.com/rust-lang/crates.io-index)", + "validator 0.7.0 (registry+https://github.com/rust-lang/crates.io-index)", + "validator_derive 0.7.1 (registry+https://github.com/rust-lang/crates.io-index)", "webfinger 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)", ] @@ -1183,6 +1190,18 @@ dependencies = [ "utf8-ranges 1.0.0 (registry+https://github.com/rust-lang/crates.io-index)", ] +[[package]] +name = "regex" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "aho-corasick 0.6.4 (registry+https://github.com/rust-lang/crates.io-index)", + "memchr 2.0.1 (registry+https://github.com/rust-lang/crates.io-index)", + "regex-syntax 0.6.1 (registry+https://github.com/rust-lang/crates.io-index)", + "thread_local 0.3.5 (registry+https://github.com/rust-lang/crates.io-index)", + "utf8-ranges 1.0.0 (registry+https://github.com/rust-lang/crates.io-index)", +] + [[package]] name = "regex-syntax" version = "0.5.5" @@ -1191,6 +1210,14 @@ dependencies = [ "ucd-util 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)", ] +[[package]] +name = "regex-syntax" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "ucd-util 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)", +] + [[package]] name = "relay" version = "0.1.1" @@ -1926,6 +1953,32 @@ dependencies = [ "rand 0.3.22 (registry+https://github.com/rust-lang/crates.io-index)", ] +[[package]] +name = "validator" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "idna 0.1.4 (registry+https://github.com/rust-lang/crates.io-index)", + "lazy_static 1.0.0 (registry+https://github.com/rust-lang/crates.io-index)", + "regex 1.0.1 (registry+https://github.com/rust-lang/crates.io-index)", + "serde 1.0.42 (registry+https://github.com/rust-lang/crates.io-index)", + "serde_derive 1.0.43 (registry+https://github.com/rust-lang/crates.io-index)", + "serde_json 1.0.16 (registry+https://github.com/rust-lang/crates.io-index)", + "url 1.7.0 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "validator_derive" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "if_chain 0.1.2 (registry+https://github.com/rust-lang/crates.io-index)", + "proc-macro2 0.4.6 (registry+https://github.com/rust-lang/crates.io-index)", + "quote 0.6.3 (registry+https://github.com/rust-lang/crates.io-index)", + "syn 0.14.2 (registry+https://github.com/rust-lang/crates.io-index)", + "validator 0.7.0 (registry+https://github.com/rust-lang/crates.io-index)", +] + [[package]] name = "vcpkg" version = "0.2.3" @@ -2064,6 +2117,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" "checksum hyper 0.11.25 (registry+https://github.com/rust-lang/crates.io-index)" = "549dbb86397490ce69d908425b9beebc85bbaad25157d67479d4995bb56fdf9a" "checksum hyper-tls 0.1.3 (registry+https://github.com/rust-lang/crates.io-index)" = "a5aa51f6ae9842239b0fac14af5f22123b8432b4cc774a44ff059fcba0f675ca" "checksum idna 0.1.4 (registry+https://github.com/rust-lang/crates.io-index)" = "014b298351066f1512874135335d62a789ffe78a9974f94b43ed5621951eaf7d" +"checksum if_chain 0.1.2 (registry+https://github.com/rust-lang/crates.io-index)" = "61bb90bdd39e3af69b0172dfc6130f6cd6332bf040fbb9bdd4401d37adbd48b8" "checksum indexmap 1.0.1 (registry+https://github.com/rust-lang/crates.io-index)" = "08173ba1e906efb6538785a8844dd496f5d34f0a2d88038e95195172fc667220" "checksum iovec 0.1.2 (registry+https://github.com/rust-lang/crates.io-index)" = "dbe6e417e7d0975db6512b90796e8ce223145ac4e33c377e4a42882a0e88bb08" "checksum isatty 0.1.7 (registry+https://github.com/rust-lang/crates.io-index)" = "a118a53ba42790ef25c82bb481ecf36e2da892646cccd361e69a6bb881e19398" @@ -2128,7 +2182,9 @@ source = "registry+https://github.com/rust-lang/crates.io-index" "checksum rand_core 0.2.1 (registry+https://github.com/rust-lang/crates.io-index)" = "edecf0f94da5551fc9b492093e30b041a891657db7940ee221f9d2f66e82eef2" "checksum redox_syscall 0.1.37 (registry+https://github.com/rust-lang/crates.io-index)" = "0d92eecebad22b767915e4d529f89f28ee96dbbf5a4810d2b844373f136417fd" "checksum regex 0.2.10 (registry+https://github.com/rust-lang/crates.io-index)" = "aec3f58d903a7d2a9dc2bf0e41a746f4530e0cab6b615494e058f67a3ef947fb" +"checksum regex 1.0.1 (registry+https://github.com/rust-lang/crates.io-index)" = "13c93d55961981ba9226a213b385216f83ab43bd6ac53ab16b2eeb47e337cf4e" "checksum regex-syntax 0.5.5 (registry+https://github.com/rust-lang/crates.io-index)" = "bd90079345f4a4c3409214734ae220fd773c6f2e8a543d07370c6c1c369cfbfb" +"checksum regex-syntax 0.6.1 (registry+https://github.com/rust-lang/crates.io-index)" = "05b06a75f5217880fc5e905952a42750bf44787e56a6c6d6852ed0992f5e1d54" "checksum relay 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)" = "1576e382688d7e9deecea24417e350d3062d97e32e45d70b1cde65994ff1489a" "checksum remove_dir_all 0.5.1 (registry+https://github.com/rust-lang/crates.io-index)" = "3488ba1b9a2084d38645c4c08276a1752dcbf2c7130d74f1569681ad5d2799c5" "checksum reqwest 0.8.5 (registry+https://github.com/rust-lang/crates.io-index)" = "241faa9a8ca28a03cbbb9815a5d085f271d4c0168a19181f106aa93240c22ddb" @@ -2210,6 +2266,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index" "checksum utf-8 0.7.2 (registry+https://github.com/rust-lang/crates.io-index)" = "f1262dfab4c30d5cb7c07026be00ee343a6cf5027fdc0104a9160f354e5db75c" "checksum utf8-ranges 1.0.0 (registry+https://github.com/rust-lang/crates.io-index)" = "662fab6525a98beff2921d7f61a39e7d59e0b425ebc7d0d9e66d316e55124122" "checksum uuid 0.5.1 (registry+https://github.com/rust-lang/crates.io-index)" = "bcc7e3b898aa6f6c08e5295b6c89258d1331e9ac578cc992fb818759951bdc22" +"checksum validator 0.7.0 (registry+https://github.com/rust-lang/crates.io-index)" = "4a8c44fecf027a477e70a86cd7f4863410adf120ca2cb13408cb099057b8e2d0" +"checksum validator_derive 0.7.1 (registry+https://github.com/rust-lang/crates.io-index)" = "708ee89305635499f793d0e2dd9d0b1b5d00daba90fdfb1392b87c7279521fab" "checksum vcpkg 0.2.3 (registry+https://github.com/rust-lang/crates.io-index)" = "7ed0f6789c8a85ca41bbc1c9d175422116a9869bd1cf31bb08e1493ecce60380" "checksum version_check 0.1.3 (registry+https://github.com/rust-lang/crates.io-index)" = "6b772017e347561807c1aa192438c5fd74242a670a6cffacc40f2defd1dc069d" "checksum void 1.0.2 (registry+https://github.com/rust-lang/crates.io-index)" = "6a02e4885ed3bc0f2de90ea6dd45ebcbb66dacffe03547fadbb0eeae2770887d" diff --git a/Cargo.toml b/Cargo.toml index efcb4b83..265c4725 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -11,18 +11,20 @@ gettext-rs = "0.4" heck = "0.3.0" rpassword = "2.0" serde_json = "1.0" +validator = "0.7" +validator_derive = "0.7" webfinger = "0.2" [dependencies.diesel] features = ["postgres", "r2d2", "chrono"] version = "*" -[dependencies.plume-models] -path = "plume-models" - [dependencies.plume-common] path = "plume-common" +[dependencies.plume-models] +path = "plume-models" + [dependencies.rocket] git = "https://github.com/SergioBenitez/Rocket" rev = "df7111143e466c18d1f56377a8d9530a5a306aba" @@ -45,4 +47,4 @@ git = "https://github.com/BaptisteGelez/rocket_i18n" rev = "5b4225d5bed5769482dc926a7e6d6b79f1217be6" [workspace] -members = ['plume-models', 'plume-common'] +members = ["plume-models", "plume-common"] diff --git a/src/main.rs b/src/main.rs index 6d55a5dc..bdf1387a 100644 --- a/src/main.rs +++ b/src/main.rs @@ -17,6 +17,9 @@ extern crate rocket_i18n; extern crate rpassword; #[macro_use] extern crate serde_json; +extern crate validator; +#[macro_use] +extern crate validator_derive; extern crate webfinger; use rocket_contrib::Template; From c81bb9ec2572e10da9c4e9ee6c87ea8f7c65a2d0 Mon Sep 17 00:00:00 2001 From: Bat Date: Fri, 29 Jun 2018 14:56:00 +0200 Subject: [PATCH 2/7] Make forms validatable --- src/routes/blogs.rs | 13 ++++++++++++- src/routes/comments.rs | 5 +++-- src/routes/posts.rs | 13 ++++++++++++- src/routes/session.rs | 5 ++++- src/routes/user.rs | 16 +++++++++++++++- 5 files changed, 46 insertions(+), 6 deletions(-) diff --git a/src/routes/blogs.rs b/src/routes/blogs.rs index 1e627439..0dc44838 100644 --- a/src/routes/blogs.rs +++ b/src/routes/blogs.rs @@ -5,6 +5,7 @@ use rocket::{ }; use rocket_contrib::Template; use serde_json; +use validator::{Validate, ValidationError}; use plume_common::activity_pub::ActivityStream; use plume_common::utils; @@ -49,11 +50,21 @@ fn new_auth() -> Flash{ utils::requires_login("You need to be logged in order to create a new blog", uri!(new)) } -#[derive(FromForm)] +#[derive(FromForm, Validate)] struct NewBlogForm { + #[validate(custom = "valid_slug")] pub title: String } +fn valid_slug(title: &str) -> Result<(), ValidationError> { + let slug = utils::make_actor_id(title.to_string()); + if slug.len() == 0 { + Err(ValidationError::new("empty_slug")) + } else { + Ok(()) + } +} + #[post("/blogs/new", data = "")] fn create(conn: DbConn, data: LenientForm, user: User) -> Redirect { let form = data.get(); diff --git a/src/routes/comments.rs b/src/routes/comments.rs index ee5112bb..78fd2852 100644 --- a/src/routes/comments.rs +++ b/src/routes/comments.rs @@ -3,6 +3,7 @@ use rocket::{ response::Redirect }; use serde_json; +use validator::Validate; use plume_common::activity_pub::broadcast; use plume_models::{ @@ -15,9 +16,10 @@ use plume_models::{ }; use inbox::Inbox; -#[derive(FromForm, Debug)] +#[derive(FromForm, Debug, Validate)] struct NewCommentForm { pub responding_to: Option, + #[validate(length(min = "1"))] pub content: String } @@ -26,7 +28,6 @@ fn create(blog_name: String, slug: String, data: LenientForm, us let blog = Blog::find_by_fqn(&*conn, blog_name.clone()).unwrap(); let post = Post::find_by_slug(&*conn, slug.clone(), blog.id).unwrap(); let form = data.get(); - println!("form: {:?}", form); let (new_comment, id) = NewComment::build() .content(form.content.clone()) diff --git a/src/routes/posts.rs b/src/routes/posts.rs index 9a518b25..a27b275b 100644 --- a/src/routes/posts.rs +++ b/src/routes/posts.rs @@ -4,6 +4,7 @@ use rocket::request::LenientForm; use rocket::response::{Redirect, Flash}; use rocket_contrib::Template; use serde_json; +use validator::{Validate, ValidationError}; use plume_common::activity_pub::{broadcast, ActivityStream}; use plume_common::utils; @@ -81,13 +82,23 @@ fn new(blog: String, user: User, conn: DbConn) -> Template { } } -#[derive(FromForm)] +#[derive(FromForm, Validate)] struct NewPostForm { + #[validate(custom = "valid_slug")] pub title: String, pub content: String, pub license: String } +fn valid_slug(title: &str) -> Result<(), ValidationError> { + let slug = title.to_string().to_kebab_case(); + if slug.len() == 0 { + Err(ValidationError::new("empty_slug")) + } else { + Ok(()) + } +} + #[post("/~//new", data = "")] fn create(blog_name: String, data: LenientForm, user: User, conn: DbConn) -> Redirect { let blog = Blog::find_by_fqn(&*conn, blog_name.to_string()).unwrap(); diff --git a/src/routes/session.rs b/src/routes/session.rs index c99548ba..1e033e90 100644 --- a/src/routes/session.rs +++ b/src/routes/session.rs @@ -5,6 +5,7 @@ use rocket::{ request::{LenientForm,FlashMessage} }; use rocket_contrib::Template; +use validator::{Validate, ValidationError}; use plume_models::{ db_conn::DbConn, @@ -32,9 +33,11 @@ fn new_message(user: Option, message: Message) -> Template { } -#[derive(FromForm)] +#[derive(FromForm, Validate)] struct LoginForm { + #[validate(length(min = "1"))] email_or_name: String, + #[validate(length(min = "8"))] password: String } diff --git a/src/routes/user.rs b/src/routes/user.rs index 3e47b8fc..0cc0576b 100644 --- a/src/routes/user.rs +++ b/src/routes/user.rs @@ -7,6 +7,7 @@ use rocket::{request::LenientForm, }; use rocket_contrib::Template; use serde_json; +use validator::{Validate, ValidationError}; use plume_common::activity_pub::{ ActivityStream, broadcast, Id, IntoId, @@ -157,14 +158,27 @@ fn update(_name: String, conn: DbConn, user: User, data: LenientForm Result<(), ValidationError> { + if form.password != form.password_confirmation { + Err(ValidationError::new("password_match")) + } else { + Ok(()) + } +} + #[post("/users/new", data = "")] fn create(conn: DbConn, data: LenientForm) -> Result { let form = data.get(); From 153400959c8900cf8e46275153c8cd98fcf75dd2 Mon Sep 17 00:00:00 2001 From: Bat Date: Fri, 6 Jul 2018 11:51:19 +0200 Subject: [PATCH 3/7] Actually validate forms --- .../2018-04-22-093322_create_instances/up.sql | 2 +- src/routes/blogs.rs | 26 +++++++--- src/routes/comments.rs | 47 ++++++++++++----- src/routes/posts.rs | 31 +++++++++--- src/routes/session.rs | 50 ++++++++++--------- src/routes/user.rs | 39 +++++++-------- 6 files changed, 122 insertions(+), 73 deletions(-) diff --git a/migrations/2018-04-22-093322_create_instances/up.sql b/migrations/2018-04-22-093322_create_instances/up.sql index e6689b0f..46fd4a3c 100644 --- a/migrations/2018-04-22-093322_create_instances/up.sql +++ b/migrations/2018-04-22-093322_create_instances/up.sql @@ -1,4 +1,4 @@ --- Your SQL goes here +l-- Your SQL goes here CREATE TABLE instances ( id SERIAL PRIMARY KEY, local_domain VARCHAR NOT NULL, diff --git a/src/routes/blogs.rs b/src/routes/blogs.rs index 0dc44838..e996e6c2 100644 --- a/src/routes/blogs.rs +++ b/src/routes/blogs.rs @@ -5,7 +5,7 @@ use rocket::{ }; use rocket_contrib::Template; use serde_json; -use validator::{Validate, ValidationError}; +use validator::{Validate, ValidationError, ValidationErrors}; use plume_common::activity_pub::ActivityStream; use plume_common::utils; @@ -66,15 +66,22 @@ fn valid_slug(title: &str) -> Result<(), ValidationError> { } #[post("/blogs/new", data = "")] -fn create(conn: DbConn, data: LenientForm, user: User) -> Redirect { +fn create(conn: DbConn, data: LenientForm, user: User) -> Result { let form = data.get(); let slug = utils::make_actor_id(form.title.to_string()); + let slug_taken_err = Blog::find_local(&*conn, slug.clone()).ok_or(ValidationError::new("existing_slug")); - if Blog::find_local(&*conn, slug.clone()).is_some() || slug.len() == 0 { - Redirect::to(uri!(new)) - } else { + let mut errors = match form.validate() { + Ok(_) => ValidationErrors::new(), + Err(e) => e + }; + if let Err(e) = slug_taken_err { + errors.add("title", e) + } + + if errors.is_empty() { let blog = Blog::insert(&*conn, NewBlog::new_local( - slug.to_string(), + slug.clone(), form.title.to_string(), String::from(""), Instance::local_id(&*conn) @@ -87,7 +94,12 @@ fn create(conn: DbConn, data: LenientForm, user: User) -> Redirect is_owner: true }); - Redirect::to(uri!(details: name = slug)) + Ok(Redirect::to(uri!(details: name = slug.clone()))) + } else { + Err(Template::render("blogs/new", json!({ + "account": user, + "errors": errors.inner() + }))) } } diff --git a/src/routes/comments.rs b/src/routes/comments.rs index 78fd2852..5248e9b9 100644 --- a/src/routes/comments.rs +++ b/src/routes/comments.rs @@ -2,6 +2,7 @@ use rocket::{ request::LenientForm, response::Redirect }; +use rocket_contrib::Template; use serde_json; use validator::Validate; @@ -24,22 +25,44 @@ struct NewCommentForm { } #[post("/~///comment", data = "")] -fn create(blog_name: String, slug: String, data: LenientForm, user: User, conn: DbConn) -> Redirect { +fn create(blog_name: String, slug: String, data: LenientForm, user: User, conn: DbConn) -> Result { let blog = Blog::find_by_fqn(&*conn, blog_name.clone()).unwrap(); let post = Post::find_by_slug(&*conn, slug.clone(), blog.id).unwrap(); let form = data.get(); + form.validate() + .map(|_| { + let (new_comment, id) = NewComment::build() + .content(form.content.clone()) + .in_response_to_id(form.responding_to.clone()) + .post(post.clone()) + .author(user.clone()) + .create(&*conn); - let (new_comment, id) = NewComment::build() - .content(form.content.clone()) - .in_response_to_id(form.responding_to.clone()) - .post(post) - .author(user.clone()) - .create(&*conn); + let instance = Instance::get_local(&*conn).unwrap(); + instance.received(&*conn, serde_json::to_value(new_comment.clone()).expect("JSON serialization error")) + .expect("We are not compatible with ourselve: local broadcast failed (new comment)"); + broadcast(&user, new_comment, user.get_followers(&*conn)); - let instance = Instance::get_local(&*conn).unwrap(); - instance.received(&*conn, serde_json::to_value(new_comment.clone()).expect("JSON serialization error")) - .expect("We are not compatible with ourselve: local broadcast failed (new comment)"); - broadcast(&user, new_comment, user.get_followers(&*conn)); + Redirect::to(format!("/~/{}/{}/#comment-{}", blog_name, slug, id)) + }) + .map_err(|errors| { + // TODO: de-duplicate this code + let comments = Comment::list_by_post(&*conn, post.id); - Redirect::to(format!("/~/{}/{}/#comment-{}", blog_name, slug, id)) + Template::render("posts/details", json!({ + "author": post.get_authors(&*conn)[0].to_json(&*conn), + "post": post, + "blog": blog, + "comments": comments.into_iter().map(|c| c.to_json(&*conn)).collect::>(), + "n_likes": post.get_likes(&*conn).len(), + "has_liked": user.has_liked(&*conn, &post), + "n_reshares": post.get_reshares(&*conn).len(), + "has_reshared": user.has_reshared(&*conn, &post), + "account": user, + "date": &post.creation_date.timestamp(), + "previous": form.responding_to.map(|r| Comment::get(&*conn, r).expect("Error retrieving previous comment").to_json(&*conn)), + "user_fqn": user.get_fqn(&*conn), + "errors": errors + })) + }) } diff --git a/src/routes/posts.rs b/src/routes/posts.rs index a27b275b..7e6cb116 100644 --- a/src/routes/posts.rs +++ b/src/routes/posts.rs @@ -4,7 +4,7 @@ use rocket::request::LenientForm; use rocket::response::{Redirect, Flash}; use rocket_contrib::Template; use serde_json; -use validator::{Validate, ValidationError}; +use validator::{Validate, ValidationError, ValidationErrors}; use plume_common::activity_pub::{broadcast, ActivityStream}; use plume_common::utils; @@ -94,22 +94,32 @@ fn valid_slug(title: &str) -> Result<(), ValidationError> { let slug = title.to_string().to_kebab_case(); if slug.len() == 0 { Err(ValidationError::new("empty_slug")) + } else if slug == "new" { + Err(ValidationError::new("invalid_slug")) } else { Ok(()) } } #[post("/~//new", data = "")] -fn create(blog_name: String, data: LenientForm, user: User, conn: DbConn) -> Redirect { +fn create(blog_name: String, data: LenientForm, user: User, conn: DbConn) -> Result { let blog = Blog::find_by_fqn(&*conn, blog_name.to_string()).unwrap(); let form = data.get(); let slug = form.title.to_string().to_kebab_case(); + let slug_taken_err = Blog::find_local(&*conn, slug.clone()).ok_or(ValidationError::new("existing_slug")); + + let mut errors = match form.validate() { + Ok(_) => ValidationErrors::new(), + Err(e) => e + }; + if let Err(e) = slug_taken_err { + errors.add("title", e) + } - if !user.is_author_in(&*conn, blog.clone()) { - Redirect::to(uri!(super::blogs::details: name = blog_name)) - } else { - if slug == "new" || Post::find_by_slug(&*conn, slug.clone(), blog.id).is_some() { - Redirect::to(uri!(new: blog = blog_name)) + if errors.is_empty() { + if !user.is_author_in(&*conn, blog.clone()) { + // actually it's not "Ok"… + Ok(Redirect::to(uri!(super::blogs::details: name = blog_name))) } else { let (content, mentions) = utils::md_to_html(form.content.to_string().as_ref()); @@ -135,7 +145,12 @@ fn create(blog_name: String, data: LenientForm, user: User, conn: D let act = post.create_activity(&*conn); broadcast(&user, act, user.get_followers(&*conn)); - Redirect::to(uri!(details: blog = blog_name, slug = slug)) + Ok(Redirect::to(uri!(details: blog = blog_name, slug = slug))) } + } else { + Err(Template::render("posts/new", json!({ + "account": user, + "errors": errors.inner() + }))) } } diff --git a/src/routes/session.rs b/src/routes/session.rs index 1e033e90..4f5062ab 100644 --- a/src/routes/session.rs +++ b/src/routes/session.rs @@ -1,11 +1,10 @@ -use gettextrs::gettext; use rocket::{ http::{Cookie, Cookies, uri::Uri}, - response::{Redirect, status::NotFound}, + response::Redirect, request::{LenientForm,FlashMessage} }; use rocket_contrib::Template; -use validator::{Validate, ValidationError}; +use validator::{Validate, ValidationError, ValidationErrors}; use plume_models::{ db_conn::DbConn, @@ -42,28 +41,33 @@ struct LoginForm { } #[post("/login", data = "")] -fn create(conn: DbConn, data: LenientForm, flash: Option, mut cookies: Cookies) -> Result> { +fn create(conn: DbConn, data: LenientForm, flash: Option, mut cookies: Cookies) -> Result { let form = data.get(); - let user = match User::find_by_email(&*conn, form.email_or_name.to_string()) { - Some(usr) => Ok(usr), - None => match User::find_local(&*conn, form.email_or_name.to_string()) { - Some(usr) => Ok(usr), - None => Err(gettext("Invalid username or password")) - } + let user = User::find_by_email(&*conn, form.email_or_name.to_string()) + .map(|u| Ok(u)) + .unwrap_or_else(|| User::find_local(&*conn, form.email_or_name.to_string()).map(|u| Ok(u)).unwrap_or(Err(()))); + + let mut errors = match form.validate() { + Ok(_) => ValidationErrors::new(), + Err(e) => e }; - match user { - Ok(usr) => { - if usr.auth(form.password.to_string()) { - cookies.add_private(Cookie::new(AUTH_COOKIE, usr.id.to_string())); - Ok(Redirect::to(Uri::new(flash - .and_then(|f| if f.name() == "callback" { Some(f.msg().to_owned()) } else { None }) - .unwrap_or("/".to_owned())) - )) - } else { - Err(NotFound(gettext("Invalid username or password"))) - } - }, - Err(e) => Err(NotFound(String::from(e))) + if let Err(_) = user.clone() { + errors.add("email_or_name", ValidationError::new("invalid_login")) + } else if !user.clone().expect("User not found").auth(form.password.clone()) { + errors.add("email_or_name", ValidationError::new("invalid_login")) + } + + if errors.is_empty() { + cookies.add_private(Cookie::new(AUTH_COOKIE, user.unwrap().id.to_string())); + Ok(Redirect::to(Uri::new(flash + .and_then(|f| if f.name() == "callback" { Some(f.msg().to_owned()) } else { None }) + .unwrap_or("/".to_owned())) + )) + } else { + Err(Template::render("session/login", json!({ + "account": user, + "errors": errors.inner() + }))) } } diff --git a/src/routes/user.rs b/src/routes/user.rs index 0cc0576b..d35a56cb 100644 --- a/src/routes/user.rs +++ b/src/routes/user.rs @@ -180,29 +180,24 @@ fn passwords_match(form: &NewUserForm) -> Result<(), ValidationError> { } #[post("/users/new", data = "")] -fn create(conn: DbConn, data: LenientForm) -> Result { +fn create(conn: DbConn, data: LenientForm) -> Result { let form = data.get(); - - if form.username.clone().len() < 1 { - Err(String::from("Username is required")) - } else if form.email.clone().len() < 1 { - Err(String::from("Email is required")) - } else if form.password.clone().len() < 8 { - Err(String::from("Password should be at least 8 characters long")) - } else if form.password == form.password_confirmation { - NewUser::new_local( - &*conn, - form.username.to_string(), - form.username.to_string(), - false, - String::from(""), - form.email.to_string(), - User::hash_pass(form.password.to_string()) - ).update_boxes(&*conn); - Ok(Redirect::to(uri!(super::session::new))) - } else { - Err(String::from("Passwords don't match")) - } + form.validate() + .map(|_| { + NewUser::new_local( + &*conn, + form.username.to_string(), + form.username.to_string(), + false, + String::from(""), + form.email.to_string(), + User::hash_pass(form.password.to_string()) + ).update_boxes(&*conn); + Redirect::to(uri!(super::session::new)) + }) + .map_err(|e| Template::render("users/new", json!({ + "errors": e.inner() + }))) } #[get("/@//outbox")] From 5f3afe900f303a558197a3be278223a329610c4f Mon Sep 17 00:00:00 2001 From: Bat Date: Fri, 6 Jul 2018 19:29:36 +0200 Subject: [PATCH 4/7] Display errors on invalid forms MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit It will probably need a bit of styling… --- Cargo.lock | 2 ++ Cargo.toml | 2 ++ src/main.rs | 3 +++ src/routes/blogs.rs | 9 ++++++--- src/routes/posts.rs | 11 +++++++---- src/routes/session.rs | 11 ++++++++--- src/routes/user.rs | 17 ++++++++++------- templates/blogs/new.html.tera | 5 +++-- templates/macros.html.tera | 9 +++++++++ templates/posts/new.html.tera | 15 +++++++++++---- templates/session/login.html.tera | 7 ++----- templates/users/new.html.tera | 16 +++++----------- 12 files changed, 68 insertions(+), 39 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 3e41543e..3304accf 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -999,6 +999,8 @@ dependencies = [ "rocket_csrf 0.1.0 (git+https://github.com/fdb-hiroshima/rocket_csrf?rev=80687a64a8b9d44e4983e63cca6d707498e92fc7)", "rocket_i18n 0.1.1 (git+https://github.com/BaptisteGelez/rocket_i18n?rev=5b4225d5bed5769482dc926a7e6d6b79f1217be6)", "rpassword 2.0.0 (registry+https://github.com/rust-lang/crates.io-index)", + "serde 1.0.42 (registry+https://github.com/rust-lang/crates.io-index)", + "serde_derive 1.0.43 (registry+https://github.com/rust-lang/crates.io-index)", "serde_json 1.0.16 (registry+https://github.com/rust-lang/crates.io-index)", "validator 0.7.0 (registry+https://github.com/rust-lang/crates.io-index)", "validator_derive 0.7.1 (registry+https://github.com/rust-lang/crates.io-index)", diff --git a/Cargo.toml b/Cargo.toml index 265c4725..a848910b 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -10,6 +10,8 @@ failure = "0.1" gettext-rs = "0.4" heck = "0.3.0" rpassword = "2.0" +serde = "1.0" +serde_derive = "1.0" serde_json = "1.0" validator = "0.7" validator_derive = "0.7" diff --git a/src/main.rs b/src/main.rs index bdf1387a..215e2101 100644 --- a/src/main.rs +++ b/src/main.rs @@ -15,6 +15,9 @@ extern crate rocket_contrib; extern crate rocket_csrf; extern crate rocket_i18n; extern crate rpassword; +extern crate serde; +#[macro_use] +extern crate serde_derive; #[macro_use] extern crate serde_json; extern crate validator; diff --git a/src/routes/blogs.rs b/src/routes/blogs.rs index e996e6c2..f7dd94a2 100644 --- a/src/routes/blogs.rs +++ b/src/routes/blogs.rs @@ -41,7 +41,9 @@ fn activity_details(name: String, conn: DbConn) -> ActivityStream { #[get("/blogs/new")] fn new(user: User) -> Template { Template::render("blogs/new", json!({ - "account": user + "account": user, + "errors": null, + "form": null })) } @@ -50,7 +52,7 @@ fn new_auth() -> Flash{ utils::requires_login("You need to be logged in order to create a new blog", uri!(new)) } -#[derive(FromForm, Validate)] +#[derive(FromForm, Validate, Serialize)] struct NewBlogForm { #[validate(custom = "valid_slug")] pub title: String @@ -98,7 +100,8 @@ fn create(conn: DbConn, data: LenientForm, user: User) -> Result Template { })) } else { Template::render("posts/new", json!({ - "account": user + "account": user, + "errors": null, + "form": null })) } } -#[derive(FromForm, Validate)] +#[derive(FromForm, Validate, Serialize)] struct NewPostForm { #[validate(custom = "valid_slug")] pub title: String, @@ -113,7 +115,7 @@ fn create(blog_name: String, data: LenientForm, user: User, conn: D Err(e) => e }; if let Err(e) = slug_taken_err { - errors.add("title", e) + errors.add("title", e); } if errors.is_empty() { @@ -150,7 +152,8 @@ fn create(blog_name: String, data: LenientForm, user: User, conn: D } else { Err(Template::render("posts/new", json!({ "account": user, - "errors": errors.inner() + "errors": errors.inner(), + "form": form }))) } } diff --git a/src/routes/session.rs b/src/routes/session.rs index 4f5062ab..e948d8e0 100644 --- a/src/routes/session.rs +++ b/src/routes/session.rs @@ -14,7 +14,9 @@ use plume_models::{ #[get("/login")] fn new(user: Option) -> Template { Template::render("session/login", json!({ - "account": user + "account": user, + "errors": null, + "form": null })) } @@ -27,7 +29,9 @@ struct Message { fn new_message(user: Option, message: Message) -> Template { Template::render("session/login", json!({ "account": user, - "message": message.m + "message": message.m, + "errors": null, + "form": null })) } @@ -66,7 +70,8 @@ fn create(conn: DbConn, data: LenientForm, flash: Option ActivityStream #[get("/users/new")] fn new(user: Option) -> Template { Template::render("users/new", json!({ - "account": user + "account": user, + "errors": null, + "form": null })) } @@ -158,16 +160,16 @@ fn update(_name: String, conn: DbConn, user: User, data: LenientForm) -> Result{{ "Create a blog" | _ }}
- - + {{ macros::input(name="title", label="Title", errors=errors, form=form) }} +
{% endblock content %} diff --git a/templates/macros.html.tera b/templates/macros.html.tera index 6a10d1ee..dc6187f9 100644 --- a/templates/macros.html.tera +++ b/templates/macros.html.tera @@ -21,3 +21,12 @@

{% endmacro post_card %} +{% macro input(name, label, errors, form, type="text") %} + + {% if errors is defined and errors[name] %} + {% for err in errors[name] %} +

{{ err.message | _ }}

+ {% endfor %} + {% endif %} + +{% endmacro input %} diff --git a/templates/posts/new.html.tera b/templates/posts/new.html.tera index 4cf457be..0878658b 100644 --- a/templates/posts/new.html.tera +++ b/templates/posts/new.html.tera @@ -7,11 +7,18 @@ {% block content %}

{{ "Create a post" | _ }}

- - + {{ macros::input(name="title", label="Title", errors=errors, form=form) }} - - + + {% if errors is defined and errors.content %} + {% for err in errors.content %} +

{{ err.message | _ }}

+ {% endfor %} + {% endif %} + + + + {{ macros::input(name="license", label="License", errors=errors, form=form) }}
diff --git a/templates/session/login.html.tera b/templates/session/login.html.tera index 4486bf3d..4a0ac027 100644 --- a/templates/session/login.html.tera +++ b/templates/session/login.html.tera @@ -10,11 +10,8 @@

{{ message }}

{% endif %}
- - - - - + {{ macros::input(name="email_or_name", label="Username or email", errors=errors, form=form) }} + {{ macros::input(name="password", label="Password", errors=errors, form=form, type="password") }}
diff --git a/templates/users/new.html.tera b/templates/users/new.html.tera index edf328c0..462d8a81 100644 --- a/templates/users/new.html.tera +++ b/templates/users/new.html.tera @@ -1,4 +1,5 @@ {% extends "base" %} +{% import "macros" as macros %} {% block title %} {{ "New Account" | _ }} @@ -7,17 +8,10 @@ {% block content %}

{{ "Create an account" | _ }}

- - - - - - - - - - - + {{ macros::input(name="username", label="Username", errors=errors, form=form) }} + {{ macros::input(name="email", label="Email", errors=errors, form=form, type="email") }} + {{ macros::input(name="password", label="Password", errors=errors, form=form, type="password") }} + {{ macros::input(name="password_confirmation", label="Password confirmation", errors=errors, form=form, type="password") }}
From e5c1b3259d425b57ff5cd49edb60f5b56c5dcc11 Mon Sep 17 00:00:00 2001 From: Bat Date: Fri, 6 Jul 2018 21:59:17 +0200 Subject: [PATCH 5/7] Make LoginForm serializable --- src/routes/session.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/routes/session.rs b/src/routes/session.rs index e948d8e0..655fc693 100644 --- a/src/routes/session.rs +++ b/src/routes/session.rs @@ -36,7 +36,7 @@ fn new_message(user: Option, message: Message) -> Template { } -#[derive(FromForm, Validate)] +#[derive(FromForm, Validate, Serialize)] struct LoginForm { #[validate(length(min = "1"))] email_or_name: String, From 3775d3a9c9b1fd2921c2f0010c63c0c6f585e1ab Mon Sep 17 00:00:00 2001 From: Bat Date: Sat, 7 Jul 2018 22:51:48 +0200 Subject: [PATCH 6/7] HTML validation + Actually associate messages to errors + Fix inverted behavior on new blog and post form --- src/routes/blogs.rs | 13 +++++++++---- src/routes/comments.rs | 2 +- src/routes/posts.rs | 12 ++++++++---- src/routes/session.rs | 4 ++-- src/routes/user.rs | 2 +- templates/blogs/new.html.tera | 2 +- templates/macros.html.tera | 6 +++--- templates/posts/new.html.tera | 6 +++--- templates/session/login.html.tera | 5 +++-- templates/users/new.html.tera | 6 +++--- 10 files changed, 34 insertions(+), 24 deletions(-) diff --git a/src/routes/blogs.rs b/src/routes/blogs.rs index f7dd94a2..9a6fcb8c 100644 --- a/src/routes/blogs.rs +++ b/src/routes/blogs.rs @@ -5,6 +5,7 @@ use rocket::{ }; use rocket_contrib::Template; use serde_json; +use std::{collections::HashMap, borrow::Cow}; use validator::{Validate, ValidationError, ValidationErrors}; use plume_common::activity_pub::ActivityStream; @@ -54,7 +55,7 @@ fn new_auth() -> Flash{ #[derive(FromForm, Validate, Serialize)] struct NewBlogForm { - #[validate(custom = "valid_slug")] + #[validate(custom(function = "valid_slug", message = "Invalid name"))] pub title: String } @@ -71,14 +72,17 @@ fn valid_slug(title: &str) -> Result<(), ValidationError> { fn create(conn: DbConn, data: LenientForm, user: User) -> Result { let form = data.get(); let slug = utils::make_actor_id(form.title.to_string()); - let slug_taken_err = Blog::find_local(&*conn, slug.clone()).ok_or(ValidationError::new("existing_slug")); let mut errors = match form.validate() { Ok(_) => ValidationErrors::new(), Err(e) => e }; - if let Err(e) = slug_taken_err { - errors.add("title", e) + if let Some(_) = Blog::find_local(&*conn, slug.clone()) { + errors.add("title", ValidationError { + code: Cow::from("existing_slug"), + message: Some(Cow::from("A blog with the same name already exists.")), + params: HashMap::new() + }); } if errors.is_empty() { @@ -98,6 +102,7 @@ fn create(conn: DbConn, data: LenientForm, user: User) -> Result, - #[validate(length(min = "1"))] + #[validate(length(min = "1", message = "Your comment can't be empty"))] pub content: String } diff --git a/src/routes/posts.rs b/src/routes/posts.rs index 182e101f..30051042 100644 --- a/src/routes/posts.rs +++ b/src/routes/posts.rs @@ -4,6 +4,7 @@ use rocket::request::LenientForm; use rocket::response::{Redirect, Flash}; use rocket_contrib::Template; use serde_json; +use std::{collections::HashMap, borrow::Cow}; use validator::{Validate, ValidationError, ValidationErrors}; use plume_common::activity_pub::{broadcast, ActivityStream}; @@ -86,7 +87,7 @@ fn new(blog: String, user: User, conn: DbConn) -> Template { #[derive(FromForm, Validate, Serialize)] struct NewPostForm { - #[validate(custom = "valid_slug")] + #[validate(custom(function = "valid_slug", message = "Invalid title"))] pub title: String, pub content: String, pub license: String @@ -108,14 +109,17 @@ fn create(blog_name: String, data: LenientForm, user: User, conn: D let blog = Blog::find_by_fqn(&*conn, blog_name.to_string()).unwrap(); let form = data.get(); let slug = form.title.to_string().to_kebab_case(); - let slug_taken_err = Blog::find_local(&*conn, slug.clone()).ok_or(ValidationError::new("existing_slug")); let mut errors = match form.validate() { Ok(_) => ValidationErrors::new(), Err(e) => e }; - if let Err(e) = slug_taken_err { - errors.add("title", e); + if let Some(_) = Post::find_by_slug(&*conn, slug.clone(), blog.id) { + errors.add("title", ValidationError { + code: Cow::from("existing_slug"), + message: Some(Cow::from("A post with the same title already exists.")), + params: HashMap::new() + }); } if errors.is_empty() { diff --git a/src/routes/session.rs b/src/routes/session.rs index 655fc693..fd79f057 100644 --- a/src/routes/session.rs +++ b/src/routes/session.rs @@ -38,9 +38,9 @@ fn new_message(user: Option, message: Message) -> Template { #[derive(FromForm, Validate, Serialize)] struct LoginForm { - #[validate(length(min = "1"))] + #[validate(length(min = "1", message = "We need an email or a username to identify you"))] email_or_name: String, - #[validate(length(min = "8"))] + #[validate(length(min = "8", message = "Your password should be at least 8 characters long"))] password: String } diff --git a/src/routes/user.rs b/src/routes/user.rs index f736901e..1b0f42e9 100644 --- a/src/routes/user.rs +++ b/src/routes/user.rs @@ -161,7 +161,7 @@ fn update(_name: String, conn: DbConn, user: User, data: LenientForm{{ "Create a blog" | _ }}
- {{ macros::input(name="title", label="Title", errors=errors, form=form) }} + {{ macros::input(name="title", label="Title", errors=errors, form=form, props='required minlength="1"') }}
diff --git a/templates/macros.html.tera b/templates/macros.html.tera index dc6187f9..868ee781 100644 --- a/templates/macros.html.tera +++ b/templates/macros.html.tera @@ -21,12 +21,12 @@

{% endmacro post_card %} -{% macro input(name, label, errors, form, type="text") %} +{% macro input(name, label, errors, form, type="text", props="") %} {% if errors is defined and errors[name] %} {% for err in errors[name] %} -

{{ err.message | _ }}

+

{{ err.message | default(value="Unknown error") }}

{% endfor %} {% endif %} - + {% endmacro input %} diff --git a/templates/posts/new.html.tera b/templates/posts/new.html.tera index 0878658b..335e2396 100644 --- a/templates/posts/new.html.tera +++ b/templates/posts/new.html.tera @@ -1,4 +1,5 @@ {% extends "base" %} +{% import "macros" as macros %} {% block title %} {{ "New post" | _ }} @@ -9,14 +10,13 @@
{{ macros::input(name="title", label="Title", errors=errors, form=form) }} - {% if errors is defined and errors.content %} {% for err in errors.content %} -

{{ err.message | _ }}

+

{{ err.message | default(value="Unknown error") | _ }}

{% endfor %} {% endif %} - + {{ macros::input(name="license", label="License", errors=errors, form=form) }} diff --git a/templates/session/login.html.tera b/templates/session/login.html.tera index 4a0ac027..bbee8c98 100644 --- a/templates/session/login.html.tera +++ b/templates/session/login.html.tera @@ -1,4 +1,5 @@ {% extends "base" %} +{% import "macros" as macros %} {% block title %} {{ "Login" | _ }} @@ -10,8 +11,8 @@

{{ message }}

{% endif %} - {{ macros::input(name="email_or_name", label="Username or email", errors=errors, form=form) }} - {{ macros::input(name="password", label="Password", errors=errors, form=form, type="password") }} + {{ macros::input(name="email_or_name", label="Username or email", errors=errors, form=form, props='minlenght="1"') }} + {{ macros::input(name="password", label="Password", errors=errors, form=form, type="password", props='minlenght="8"') }}
diff --git a/templates/users/new.html.tera b/templates/users/new.html.tera index 462d8a81..b0aef4e1 100644 --- a/templates/users/new.html.tera +++ b/templates/users/new.html.tera @@ -8,10 +8,10 @@ {% block content %}

{{ "Create an account" | _ }}

- {{ macros::input(name="username", label="Username", errors=errors, form=form) }} + {{ macros::input(name="username", label="Username", errors=errors, form=form, props='minlenght="1"') }} {{ macros::input(name="email", label="Email", errors=errors, form=form, type="email") }} - {{ macros::input(name="password", label="Password", errors=errors, form=form, type="password") }} - {{ macros::input(name="password_confirmation", label="Password confirmation", errors=errors, form=form, type="password") }} + {{ macros::input(name="password", label="Password", errors=errors, form=form, type="password", props='minlenght="8"') }} + {{ macros::input(name="password_confirmation", label="Password confirmation", errors=errors, form=form, type="password", props='minlenght="8"') }}
From 06d590ff3b1c1087da93df94fbd0400a819af540 Mon Sep 17 00:00:00 2001 From: Bat Date: Sat, 7 Jul 2018 22:57:53 +0200 Subject: [PATCH 7/7] Make form errors i18nalizable --- po/plume.pot | 34 ++++++++++++++++++++++++++++++++++ templates/macros.html.tera | 2 +- 2 files changed, 35 insertions(+), 1 deletion(-) diff --git a/po/plume.pot b/po/plume.pot index e1599c75..ccb727e3 100644 --- a/po/plume.pot +++ b/po/plume.pot @@ -281,3 +281,37 @@ msgstr "" msgid "Your comment" msgstr "" + +msgid "Unknown error" +msgstr "" + +msgid "Invalid name" +msgstr "" + +msgid "A blog with the same name already exists." +msgstr "" + +msgid "Your comment can't be empty" +msgstr "" + +msgid "A post with the same title already exists." +msgstr "" + +msgid "We need an email or a username to identify you" +msgstr "" + +msgid "Your password should be at least 8 characters long" +msgstr "" + +msgid "Passwords are not matching" +msgstr "" + +msgid "Username can't be empty" +msgstr "" + +msgid "Invalid email" +msgstr "" + +msgid "Password should be at least 8 characters long" +msgstr "" + diff --git a/templates/macros.html.tera b/templates/macros.html.tera index 868ee781..c12a6799 100644 --- a/templates/macros.html.tera +++ b/templates/macros.html.tera @@ -25,7 +25,7 @@ {% if errors is defined and errors[name] %} {% for err in errors[name] %} -

{{ err.message | default(value="Unknown error") }}

+

{{ err.message | default(value="Unknown error") | _ }}

{% endfor %} {% endif %}