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)",
]
[[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"

View file

@ -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"]

View file

@ -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,

View file

@ -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 ""

View file

@ -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;

View file

@ -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<CustomGroup> {
#[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<Redirect>{
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 = "<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 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<NewBlogForm>, 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
})))
}
}

View file

@ -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<i32>,
#[validate(length(min = "1", message = "Your comment can't be empty"))]
pub content: String
}
#[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 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::<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_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("/~/<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 form = data.get();
let slug = form.title.to_string().to_kebab_case();
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))
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 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<NewPostForm>, 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
})))
}
}

View file

@ -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<User>) -> 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<User>, 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 = "<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 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
})))
}
}

View file

@ -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<CustomPerson>
#[get("/users/new")]
fn new(user: Option<User>) -> 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<UpdateUserF
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 {
#[validate(length(min = "1", message = "Username can't be empty"))]
username: String,
#[validate(email(message = "Invalid email"))]
email: String,
#[validate(length(min = "8", message = "Password should be at least 8 characters long"))]
password: String,
#[validate(length(min = "8", message = "Password should be at least 8 characters long"))]
password_confirmation: String
}
#[post("/users/new", data = "<data>")]
fn create(conn: DbConn, data: LenientForm<NewUserForm>) -> Result<Redirect, String> {
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 = "<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")]
fn outbox(name: String, conn: DbConn) -> ActivityStream<OrderedCollection> {
let user = User::find_local(&*conn, name).unwrap();

View file

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

View file

@ -21,3 +21,12 @@
</p>
</div>
{% 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" %}
{% import "macros" as macros %}
{% block title %}
{{ "New post" | _ }}
@ -7,11 +8,17 @@
{% block content %}
<h1>{{ "Create a post" | _ }}</h1>
<form class="new-post" method="post">
<input type="text" class="title" name="title" placeholder="{{ "Title" | _ }}">
<textarea name="content" placeholder="{{ "Content" | _ }}"></textarea>
{{ macros::input(name="title", label="Title", errors=errors, form=form) }}
<label for="license">{{ "License" | _ }}</label>
<input type="text" id="licence" name="license" />
{% if errors is defined and errors.content %}
{% 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" | _ }}" />
</form>

View file

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

View file

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