diff --git a/Cargo.lock b/Cargo.lock index 26dfe243..3173b306 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" @@ -994,7 +999,11 @@ dependencies = [ "rocket_csrf 0.1.0 (git+https://github.com/fdb-hiroshima/rocket_csrf?rev=13ca47ef73be86cef9caca30c516e4e95f3051ce)", "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)", "webfinger 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)", ] @@ -1183,6 +1192,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 +1212,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 +1955,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 +2119,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 +2184,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 +2268,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 85261db3..75522b0c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -10,19 +10,23 @@ 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" 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 +49,4 @@ git = "https://github.com/BaptisteGelez/rocket_i18n" rev = "5b4225d5bed5769482dc926a7e6d6b79f1217be6" [workspace] -members = ['plume-models', 'plume-common'] +members = ["plume-models", "plume-common"] 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/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/src/main.rs b/src/main.rs index 6d55a5dc..215e2101 100644 --- a/src/main.rs +++ b/src/main.rs @@ -15,8 +15,14 @@ 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; +#[macro_use] +extern crate validator_derive; extern crate webfinger; use rocket_contrib::Template; diff --git a/src/routes/blogs.rs b/src/routes/blogs.rs index 1e627439..9a6fcb8c 100644 --- a/src/routes/blogs.rs +++ b/src/routes/blogs.rs @@ -5,6 +5,8 @@ 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; use plume_common::utils; @@ -40,7 +42,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 })) } @@ -49,21 +53,41 @@ 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, Serialize)] struct NewBlogForm { + #[validate(custom(function = "valid_slug", message = "Invalid name"))] 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 { +fn create(conn: DbConn, data: LenientForm, user: User) -> Result { let form = data.get(); let slug = utils::make_actor_id(form.title.to_string()); - 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 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() { let blog = Blog::insert(&*conn, NewBlog::new_local( - slug.to_string(), + slug.clone(), form.title.to_string(), String::from(""), Instance::local_id(&*conn) @@ -76,7 +100,14 @@ 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 { + println!("{:?}", errors); + Err(Template::render("blogs/new", json!({ + "account": user, + "errors": errors.inner(), + "form": form + }))) } } diff --git a/src/routes/comments.rs b/src/routes/comments.rs index ee5112bb..d08998ed 100644 --- a/src/routes/comments.rs +++ b/src/routes/comments.rs @@ -2,7 +2,9 @@ use rocket::{ request::LenientForm, response::Redirect }; +use rocket_contrib::Template; use serde_json; +use validator::Validate; use plume_common::activity_pub::broadcast; use plume_models::{ @@ -15,30 +17,52 @@ use plume_models::{ }; use inbox::Inbox; -#[derive(FromForm, Debug)] +#[derive(FromForm, Debug, Validate)] struct NewCommentForm { pub responding_to: Option, + #[validate(length(min = "1", message = "Your comment can't be empty"))] pub content: String } #[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(); - println!("form: {:?}", form); + 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 9a518b25..30051042 100644 --- a/src/routes/posts.rs +++ b/src/routes/posts.rs @@ -4,6 +4,8 @@ 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}; use plume_common::utils; @@ -76,29 +78,54 @@ fn new(blog: String, user: User, conn: DbConn) -> Template { })) } else { Template::render("posts/new", json!({ - "account": user + "account": user, + "errors": null, + "form": null })) } } -#[derive(FromForm)] +#[derive(FromForm, Validate, Serialize)] struct NewPostForm { + #[validate(custom(function = "valid_slug", message = "Invalid title"))] 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 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 mut errors = match form.validate() { + Ok(_) => ValidationErrors::new(), + Err(e) => 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 !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()); @@ -124,7 +151,13 @@ 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(), + "form": form + }))) } } diff --git a/src/routes/session.rs b/src/routes/session.rs index c99548ba..fd79f057 100644 --- a/src/routes/session.rs +++ b/src/routes/session.rs @@ -1,10 +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, ValidationErrors}; use plume_models::{ db_conn::DbConn, @@ -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,40 +29,50 @@ 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 })) } -#[derive(FromForm)] +#[derive(FromForm, Validate, Serialize)] struct LoginForm { + #[validate(length(min = "1", message = "We need an email or a username to identify you"))] email_or_name: String, + #[validate(length(min = "8", message = "Your password should be at least 8 characters long"))] password: String } #[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(), + "form": form + }))) } } diff --git a/src/routes/user.rs b/src/routes/user.rs index 3e47b8fc..1b0f42e9 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, @@ -120,7 +121,9 @@ fn activity_details(name: String, conn: DbConn) -> ActivityStream #[get("/users/new")] fn new(user: Option) -> Template { Template::render("users/new", json!({ - "account": user + "account": user, + "errors": null, + "form": null })) } @@ -157,40 +160,49 @@ fn update(_name: String, conn: DbConn, user: User, 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))) +fn passwords_match(form: &NewUserForm) -> Result<(), ValidationError> { + if form.password != form.password_confirmation { + Err(ValidationError::new("password_match")) } else { - Err(String::from("Passwords don't match")) + Ok(()) } } +#[post("/users/new", data = "")] +fn create(conn: DbConn, data: LenientForm) -> Result { + let form = data.get(); + 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(), + "form": form + }))) +} + #[get("/@//outbox")] fn outbox(name: String, conn: DbConn) -> ActivityStream { let user = User::find_local(&*conn, name).unwrap(); diff --git a/templates/blogs/new.html.tera b/templates/blogs/new.html.tera index 0dd78775..9c1f12ec 100644 --- a/templates/blogs/new.html.tera +++ b/templates/blogs/new.html.tera @@ -1,4 +1,5 @@ {% extends "base" %} +{% import "macros" as macros %} {% block title %} {{ "New blog" | _ }} @@ -7,8 +8,8 @@ {% block content %}

{{ "Create a blog" | _ }}

- - + {{ macros::input(name="title", label="Title", errors=errors, form=form, props='required minlength="1"') }} +
{% endblock content %} diff --git a/templates/macros.html.tera b/templates/macros.html.tera index 6a10d1ee..c12a6799 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", props="") %} + + {% if errors is defined and errors[name] %} + {% for err in errors[name] %} +

{{ 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 4cf457be..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" | _ }} @@ -7,11 +8,17 @@ {% 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 | 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 4486bf3d..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,11 +11,8 @@

{{ message }}

{% endif %}
- - - - - + {{ 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 edf328c0..b0aef4e1 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, 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", props='minlenght="8"') }} + {{ macros::input(name="password_confirmation", label="Password confirmation", errors=errors, form=form, type="password", props='minlenght="8"') }}