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" | _ }}
{% 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") %}
+ {{ label | _ }}
+ {% 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" | _ }}
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 %}
- {{ "Username or email" | _ }}
-
-
- {{ "Password" | _ }}
-
+ {{ 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" | _ }}
- {{ "Username" | _ }}
-
-
- {{ "Email" | _ }}
-
-
- {{ "Password" | _ }}
-
-
- {{ "Password confirmation" | _ }}
-
+ {{ 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="") %}
{{ label | _ }}
{% 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) }}
- {{ label | _ }}
{% 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 @@
{{ label | _ }}
{% 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 %}