Merge pull request #111 from Plume-org/form-validation

Form validation
This commit is contained in:
Baptiste Gelez 2018-07-08 14:28:47 +02:00 committed by GitHub
commit 6fe70cd723
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
15 changed files with 332 additions and 107 deletions

60
Cargo.lock generated
View file

@ -592,6 +592,11 @@ dependencies = [
"unicode-normalization 0.1.5 (registry+https://github.com/rust-lang/crates.io-index)", "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]] [[package]]
name = "indexmap" name = "indexmap"
version = "1.0.1" 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_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)", "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)", "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)", "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)", "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)", "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]] [[package]]
name = "regex-syntax" name = "regex-syntax"
version = "0.5.5" version = "0.5.5"
@ -1191,6 +1212,14 @@ dependencies = [
"ucd-util 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)", "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]] [[package]]
name = "relay" name = "relay"
version = "0.1.1" version = "0.1.1"
@ -1926,6 +1955,32 @@ dependencies = [
"rand 0.3.22 (registry+https://github.com/rust-lang/crates.io-index)", "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]] [[package]]
name = "vcpkg" name = "vcpkg"
version = "0.2.3" 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 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 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 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 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 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" "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 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 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 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.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 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 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" "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 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 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 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 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 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" "checksum void 1.0.2 (registry+https://github.com/rust-lang/crates.io-index)" = "6a02e4885ed3bc0f2de90ea6dd45ebcbb66dacffe03547fadbb0eeae2770887d"

View file

@ -10,19 +10,23 @@ failure = "0.1"
gettext-rs = "0.4" gettext-rs = "0.4"
heck = "0.3.0" heck = "0.3.0"
rpassword = "2.0" rpassword = "2.0"
serde = "1.0"
serde_derive = "1.0"
serde_json = "1.0" serde_json = "1.0"
validator = "0.7"
validator_derive = "0.7"
webfinger = "0.2" webfinger = "0.2"
[dependencies.diesel] [dependencies.diesel]
features = ["postgres", "r2d2", "chrono"] features = ["postgres", "r2d2", "chrono"]
version = "*" version = "*"
[dependencies.plume-models]
path = "plume-models"
[dependencies.plume-common] [dependencies.plume-common]
path = "plume-common" path = "plume-common"
[dependencies.plume-models]
path = "plume-models"
[dependencies.rocket] [dependencies.rocket]
git = "https://github.com/SergioBenitez/Rocket" git = "https://github.com/SergioBenitez/Rocket"
rev = "df7111143e466c18d1f56377a8d9530a5a306aba" rev = "df7111143e466c18d1f56377a8d9530a5a306aba"
@ -45,4 +49,4 @@ git = "https://github.com/BaptisteGelez/rocket_i18n"
rev = "5b4225d5bed5769482dc926a7e6d6b79f1217be6" rev = "5b4225d5bed5769482dc926a7e6d6b79f1217be6"
[workspace] [workspace]
members = ['plume-models', 'plume-common'] members = ["plume-models", "plume-common"]

View file

@ -1,4 +1,4 @@
-- Your SQL goes here l-- Your SQL goes here
CREATE TABLE instances ( CREATE TABLE instances (
id SERIAL PRIMARY KEY, id SERIAL PRIMARY KEY,
local_domain VARCHAR NOT NULL, local_domain VARCHAR NOT NULL,

View file

@ -281,3 +281,37 @@ msgstr ""
msgid "Your comment" msgid "Your comment"
msgstr "" 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 ""

View file

@ -15,8 +15,14 @@ extern crate rocket_contrib;
extern crate rocket_csrf; extern crate rocket_csrf;
extern crate rocket_i18n; extern crate rocket_i18n;
extern crate rpassword; extern crate rpassword;
extern crate serde;
#[macro_use]
extern crate serde_derive;
#[macro_use] #[macro_use]
extern crate serde_json; extern crate serde_json;
extern crate validator;
#[macro_use]
extern crate validator_derive;
extern crate webfinger; extern crate webfinger;
use rocket_contrib::Template; use rocket_contrib::Template;

View file

@ -5,6 +5,8 @@ use rocket::{
}; };
use rocket_contrib::Template; use rocket_contrib::Template;
use serde_json; use serde_json;
use std::{collections::HashMap, borrow::Cow};
use validator::{Validate, ValidationError, ValidationErrors};
use plume_common::activity_pub::ActivityStream; use plume_common::activity_pub::ActivityStream;
use plume_common::utils; use plume_common::utils;
@ -40,7 +42,9 @@ fn activity_details(name: String, conn: DbConn) -> ActivityStream<CustomGroup> {
#[get("/blogs/new")] #[get("/blogs/new")]
fn new(user: User) -> Template { fn new(user: User) -> Template {
Template::render("blogs/new", json!({ Template::render("blogs/new", json!({
"account": user "account": user,
"errors": null,
"form": null
})) }))
} }
@ -49,21 +53,41 @@ fn new_auth() -> Flash<Redirect>{
utils::requires_login("You need to be logged in order to create a new blog", uri!(new)) 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 { struct NewBlogForm {
#[validate(custom(function = "valid_slug", message = "Invalid name"))]
pub title: String 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 = "<data>")] #[post("/blogs/new", data = "<data>")]
fn create(conn: DbConn, data: LenientForm<NewBlogForm>, user: User) -> Redirect { fn create(conn: DbConn, data: LenientForm<NewBlogForm>, user: User) -> Result<Redirect, Template> {
let form = data.get(); let form = data.get();
let slug = utils::make_actor_id(form.title.to_string()); let slug = utils::make_actor_id(form.title.to_string());
if Blog::find_local(&*conn, slug.clone()).is_some() || slug.len() == 0 { let mut errors = match form.validate() {
Redirect::to(uri!(new)) Ok(_) => ValidationErrors::new(),
} else { 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( let blog = Blog::insert(&*conn, NewBlog::new_local(
slug.to_string(), slug.clone(),
form.title.to_string(), form.title.to_string(),
String::from(""), String::from(""),
Instance::local_id(&*conn) Instance::local_id(&*conn)
@ -76,7 +100,14 @@ fn create(conn: DbConn, data: LenientForm<NewBlogForm>, user: User) -> Redirect
is_owner: true 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
})))
} }
} }

View file

@ -2,7 +2,9 @@ use rocket::{
request::LenientForm, request::LenientForm,
response::Redirect response::Redirect
}; };
use rocket_contrib::Template;
use serde_json; use serde_json;
use validator::Validate;
use plume_common::activity_pub::broadcast; use plume_common::activity_pub::broadcast;
use plume_models::{ use plume_models::{
@ -15,30 +17,52 @@ use plume_models::{
}; };
use inbox::Inbox; use inbox::Inbox;
#[derive(FromForm, Debug)] #[derive(FromForm, Debug, Validate)]
struct NewCommentForm { struct NewCommentForm {
pub responding_to: Option<i32>, pub responding_to: Option<i32>,
#[validate(length(min = "1", message = "Your comment can't be empty"))]
pub content: String pub content: String
} }
#[post("/~/<blog_name>/<slug>/comment", data = "<data>")] #[post("/~/<blog_name>/<slug>/comment", data = "<data>")]
fn create(blog_name: String, slug: String, data: LenientForm<NewCommentForm>, user: User, conn: DbConn) -> Redirect { fn create(blog_name: String, slug: String, data: LenientForm<NewCommentForm>, user: User, conn: DbConn) -> Result<Redirect, Template> {
let blog = Blog::find_by_fqn(&*conn, blog_name.clone()).unwrap(); let blog = Blog::find_by_fqn(&*conn, blog_name.clone()).unwrap();
let post = Post::find_by_slug(&*conn, slug.clone(), blog.id).unwrap(); let post = Post::find_by_slug(&*conn, slug.clone(), blog.id).unwrap();
let form = data.get(); 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() let instance = Instance::get_local(&*conn).unwrap();
.content(form.content.clone()) instance.received(&*conn, serde_json::to_value(new_comment.clone()).expect("JSON serialization error"))
.in_response_to_id(form.responding_to.clone()) .expect("We are not compatible with ourselve: local broadcast failed (new comment)");
.post(post) broadcast(&user, new_comment, user.get_followers(&*conn));
.author(user.clone())
.create(&*conn);
let instance = Instance::get_local(&*conn).unwrap(); Redirect::to(format!("/~/{}/{}/#comment-{}", blog_name, slug, id))
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)"); .map_err(|errors| {
broadcast(&user, new_comment, user.get_followers(&*conn)); // 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::<Vec<serde_json::Value>>(),
"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
}))
})
} }

View file

@ -4,6 +4,8 @@ use rocket::request::LenientForm;
use rocket::response::{Redirect, Flash}; use rocket::response::{Redirect, Flash};
use rocket_contrib::Template; use rocket_contrib::Template;
use serde_json; use serde_json;
use std::{collections::HashMap, borrow::Cow};
use validator::{Validate, ValidationError, ValidationErrors};
use plume_common::activity_pub::{broadcast, ActivityStream}; use plume_common::activity_pub::{broadcast, ActivityStream};
use plume_common::utils; use plume_common::utils;
@ -76,29 +78,54 @@ fn new(blog: String, user: User, conn: DbConn) -> Template {
})) }))
} else { } else {
Template::render("posts/new", json!({ Template::render("posts/new", json!({
"account": user "account": user,
"errors": null,
"form": null
})) }))
} }
} }
#[derive(FromForm)] #[derive(FromForm, Validate, Serialize)]
struct NewPostForm { struct NewPostForm {
#[validate(custom(function = "valid_slug", message = "Invalid title"))]
pub title: String, pub title: String,
pub content: String, pub content: String,
pub license: 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("/~/<blog_name>/new", data = "<data>")] #[post("/~/<blog_name>/new", data = "<data>")]
fn create(blog_name: String, data: LenientForm<NewPostForm>, user: User, conn: DbConn) -> Redirect { fn create(blog_name: String, data: LenientForm<NewPostForm>, user: User, conn: DbConn) -> Result<Redirect, Template> {
let blog = Blog::find_by_fqn(&*conn, blog_name.to_string()).unwrap(); let blog = Blog::find_by_fqn(&*conn, blog_name.to_string()).unwrap();
let form = data.get(); let form = data.get();
let slug = form.title.to_string().to_kebab_case(); 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()) { if errors.is_empty() {
Redirect::to(uri!(super::blogs::details: name = blog_name)) if !user.is_author_in(&*conn, blog.clone()) {
} else { // actually it's not "Ok"…
if slug == "new" || Post::find_by_slug(&*conn, slug.clone(), blog.id).is_some() { Ok(Redirect::to(uri!(super::blogs::details: name = blog_name)))
Redirect::to(uri!(new: blog = blog_name))
} else { } else {
let (content, mentions) = utils::md_to_html(form.content.to_string().as_ref()); let (content, mentions) = utils::md_to_html(form.content.to_string().as_ref());
@ -124,7 +151,13 @@ fn create(blog_name: String, data: LenientForm<NewPostForm>, user: User, conn: D
let act = post.create_activity(&*conn); let act = post.create_activity(&*conn);
broadcast(&user, act, user.get_followers(&*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
})))
} }
} }

View file

@ -1,10 +1,10 @@
use gettextrs::gettext;
use rocket::{ use rocket::{
http::{Cookie, Cookies, uri::Uri}, http::{Cookie, Cookies, uri::Uri},
response::{Redirect, status::NotFound}, response::Redirect,
request::{LenientForm,FlashMessage} request::{LenientForm,FlashMessage}
}; };
use rocket_contrib::Template; use rocket_contrib::Template;
use validator::{Validate, ValidationError, ValidationErrors};
use plume_models::{ use plume_models::{
db_conn::DbConn, db_conn::DbConn,
@ -14,7 +14,9 @@ use plume_models::{
#[get("/login")] #[get("/login")]
fn new(user: Option<User>) -> Template { fn new(user: Option<User>) -> Template {
Template::render("session/login", json!({ Template::render("session/login", json!({
"account": user "account": user,
"errors": null,
"form": null
})) }))
} }
@ -27,40 +29,50 @@ struct Message {
fn new_message(user: Option<User>, message: Message) -> Template { fn new_message(user: Option<User>, message: Message) -> Template {
Template::render("session/login", json!({ Template::render("session/login", json!({
"account": user, "account": user,
"message": message.m "message": message.m,
"errors": null,
"form": null
})) }))
} }
#[derive(FromForm)] #[derive(FromForm, Validate, Serialize)]
struct LoginForm { struct LoginForm {
#[validate(length(min = "1", message = "We need an email or a username to identify you"))]
email_or_name: String, email_or_name: String,
#[validate(length(min = "8", message = "Your password should be at least 8 characters long"))]
password: String password: String
} }
#[post("/login", data = "<data>")] #[post("/login", data = "<data>")]
fn create(conn: DbConn, data: LenientForm<LoginForm>, flash: Option<FlashMessage>, mut cookies: Cookies) -> Result<Redirect, NotFound<String>> { fn create(conn: DbConn, data: LenientForm<LoginForm>, flash: Option<FlashMessage>, mut cookies: Cookies) -> Result<Redirect, Template> {
let form = data.get(); let form = data.get();
let user = match User::find_by_email(&*conn, form.email_or_name.to_string()) { let user = User::find_by_email(&*conn, form.email_or_name.to_string())
Some(usr) => Ok(usr), .map(|u| Ok(u))
None => match User::find_local(&*conn, form.email_or_name.to_string()) { .unwrap_or_else(|| User::find_local(&*conn, form.email_or_name.to_string()).map(|u| Ok(u)).unwrap_or(Err(())));
Some(usr) => Ok(usr),
None => Err(gettext("Invalid username or password")) let mut errors = match form.validate() {
} Ok(_) => ValidationErrors::new(),
Err(e) => e
}; };
match user { if let Err(_) = user.clone() {
Ok(usr) => { errors.add("email_or_name", ValidationError::new("invalid_login"))
if usr.auth(form.password.to_string()) { } else if !user.clone().expect("User not found").auth(form.password.clone()) {
cookies.add_private(Cookie::new(AUTH_COOKIE, usr.id.to_string())); errors.add("email_or_name", ValidationError::new("invalid_login"))
Ok(Redirect::to(Uri::new(flash }
.and_then(|f| if f.name() == "callback" { Some(f.msg().to_owned()) } else { None })
.unwrap_or("/".to_owned())) if errors.is_empty() {
)) cookies.add_private(Cookie::new(AUTH_COOKIE, user.unwrap().id.to_string()));
} else { Ok(Redirect::to(Uri::new(flash
Err(NotFound(gettext("Invalid username or password"))) .and_then(|f| if f.name() == "callback" { Some(f.msg().to_owned()) } else { None })
} .unwrap_or("/".to_owned()))
}, ))
Err(e) => Err(NotFound(String::from(e))) } else {
Err(Template::render("session/login", json!({
"account": user,
"errors": errors.inner(),
"form": form
})))
} }
} }

View file

@ -7,6 +7,7 @@ use rocket::{request::LenientForm,
}; };
use rocket_contrib::Template; use rocket_contrib::Template;
use serde_json; use serde_json;
use validator::{Validate, ValidationError};
use plume_common::activity_pub::{ use plume_common::activity_pub::{
ActivityStream, broadcast, Id, IntoId, ActivityStream, broadcast, Id, IntoId,
@ -120,7 +121,9 @@ fn activity_details(name: String, conn: DbConn) -> ActivityStream<CustomPerson>
#[get("/users/new")] #[get("/users/new")]
fn new(user: Option<User>) -> Template { fn new(user: Option<User>) -> Template {
Template::render("users/new", json!({ 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<UpdateUserF
Redirect::to(uri!(me)) Redirect::to(uri!(me))
} }
#[derive(FromForm)] #[derive(FromForm, Serialize, Validate)]
#[validate(schema(function = "passwords_match", skip_on_field_errors = "false", message = "Passwords are not matching"))]
struct NewUserForm { struct NewUserForm {
#[validate(length(min = "1", message = "Username can't be empty"))]
username: String, username: String,
#[validate(email(message = "Invalid email"))]
email: String, email: String,
#[validate(length(min = "8", message = "Password should be at least 8 characters long"))]
password: String, password: String,
#[validate(length(min = "8", message = "Password should be at least 8 characters long"))]
password_confirmation: String password_confirmation: String
} }
#[post("/users/new", data = "<data>")] fn passwords_match(form: &NewUserForm) -> Result<(), ValidationError> {
fn create(conn: DbConn, data: LenientForm<NewUserForm>) -> Result<Redirect, String> { if form.password != form.password_confirmation {
let form = data.get(); Err(ValidationError::new("password_match"))
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 { } else {
Err(String::from("Passwords don't match")) Ok(())
} }
} }
#[post("/users/new", data = "<data>")]
fn create(conn: DbConn, data: LenientForm<NewUserForm>) -> Result<Redirect, Template> {
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("/@/<name>/outbox")] #[get("/@/<name>/outbox")]
fn outbox(name: String, conn: DbConn) -> ActivityStream<OrderedCollection> { fn outbox(name: String, conn: DbConn) -> ActivityStream<OrderedCollection> {
let user = User::find_local(&*conn, name).unwrap(); let user = User::find_local(&*conn, name).unwrap();

View file

@ -1,4 +1,5 @@
{% extends "base" %} {% extends "base" %}
{% import "macros" as macros %}
{% block title %} {% block title %}
{{ "New blog" | _ }} {{ "New blog" | _ }}
@ -7,8 +8,8 @@
{% block content %} {% block content %}
<h1>{{ "Create a blog" | _ }}</h1> <h1>{{ "Create a blog" | _ }}</h1>
<form method="post"> <form method="post">
<label for="title">{{ "Title" | _ }}</label> {{ macros::input(name="title", label="Title", errors=errors, form=form, props='required minlength="1"') }}
<input type="text" id="title" name="title" />
<input type="submit" value="{{ "Create blog" | _ }}"/> <input type="submit" value="{{ "Create blog" | _ }}"/>
</form> </form>
{% endblock content %} {% endblock content %}

View file

@ -21,3 +21,12 @@
</p> </p>
</div> </div>
{% endmacro post_card %} {% endmacro post_card %}
{% macro input(name, label, errors, form, type="text", props="") %}
<label for="{{ name }}">{{ label | _ }}</label>
{% if errors is defined and errors[name] %}
{% for err in errors[name] %}
<p class="error">{{ err.message | default(value="Unknown error") | _ }}</p>
{% endfor %}
{% endif %}
<input type="{{ type }}" id="{{ name }}" name="{{ name }}" value="{{ form[name] | default(value="") }}" {{ props | safe }}/>
{% endmacro input %}

View file

@ -1,4 +1,5 @@
{% extends "base" %} {% extends "base" %}
{% import "macros" as macros %}
{% block title %} {% block title %}
{{ "New post" | _ }} {{ "New post" | _ }}
@ -7,11 +8,17 @@
{% block content %} {% block content %}
<h1>{{ "Create a post" | _ }}</h1> <h1>{{ "Create a post" | _ }}</h1>
<form class="new-post" method="post"> <form class="new-post" method="post">
<input type="text" class="title" name="title" placeholder="{{ "Title" | _ }}"> {{ macros::input(name="title", label="Title", errors=errors, form=form) }}
<textarea name="content" placeholder="{{ "Content" | _ }}"></textarea>
<label for="license">{{ "License" | _ }}</label> {% if errors is defined and errors.content %}
<input type="text" id="licence" name="license" /> {% for err in errors.content %}
<p class="error">{{ err.message | default(value="Unknown error") | _ }}</p>
{% endfor %}
{% endif %}
<textarea id="content" name="content" placeholder="{{ "Content" | _ }}" value="{{ form.content | default(value="") }}"></textarea>
{{ macros::input(name="license", label="License", errors=errors, form=form) }}
<input type="submit" value="{{ "Publish" | _ }}" /> <input type="submit" value="{{ "Publish" | _ }}" />
</form> </form>

View file

@ -1,4 +1,5 @@
{% extends "base" %} {% extends "base" %}
{% import "macros" as macros %}
{% block title %} {% block title %}
{{ "Login" | _ }} {{ "Login" | _ }}
@ -10,11 +11,8 @@
<p>{{ message }}</p> <p>{{ message }}</p>
{% endif %} {% endif %}
<form method="post"> <form method="post">
<label for="email_or_name">{{ "Username or email" | _ }}</label> {{ macros::input(name="email_or_name", label="Username or email", errors=errors, form=form, props='minlenght="1"') }}
<input type="text" id="email_or_name" name="email_or_name" /> {{ macros::input(name="password", label="Password", errors=errors, form=form, type="password", props='minlenght="8"') }}
<label for="password">{{ "Password" | _ }}</label>
<input type="password" id="password" name="password" />
<input type="submit" value="{{ "Login" | _ }}" /> <input type="submit" value="{{ "Login" | _ }}" />
</form> </form>

View file

@ -1,4 +1,5 @@
{% extends "base" %} {% extends "base" %}
{% import "macros" as macros %}
{% block title %} {% block title %}
{{ "New Account" | _ }} {{ "New Account" | _ }}
@ -7,17 +8,10 @@
{% block content %} {% block content %}
<h1>{{ "Create an account" | _ }}</h1> <h1>{{ "Create an account" | _ }}</h1>
<form method="post"> <form method="post">
<label for="username">{{ "Username" | _ }}</label> {{ macros::input(name="username", label="Username", errors=errors, form=form, props='minlenght="1"') }}
<input type="text" id="username" name="username" /> {{ 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"') }}
<label for="email">{{ "Email" | _ }}</label> {{ macros::input(name="password_confirmation", label="Password confirmation", errors=errors, form=form, type="password", props='minlenght="8"') }}
<input type="email" id="email" name="email" />
<label for="password">{{ "Password" | _ }}</label>
<input type="password" id="password" name="password" />
<label for="password_confirmation">{{ "Password confirmation" | _ }}</label>
<input type="password" id="password_confirmation" name="password_confirmation" />
<input type="submit" value="{{ "Create account" | _ }}" /> <input type="submit" value="{{ "Create account" | _ }}" />
</form> </form>