diff --git a/README.md b/README.md index b3f8d11b0..ce8672e66 100644 --- a/README.md +++ b/README.md @@ -35,6 +35,8 @@ We have a twitter alternative (mastodon), a facebook alternative (friendica), so - [Recursive query for adjacency list for nested comments](https://stackoverflow.com/questions/192220/what-is-the-most-efficient-elegant-way-to-parse-a-flat-table-into-a-tree/192462#192462) - https://github.com/sparksuite/simplemde-markdown-editor - [Sticky Sidebar](https://stackoverflow.com/questions/38382043/how-to-use-css-position-sticky-to-keep-a-sidebar-visible-with-bootstrap-4/49111934) +- [RXJS websocket](https://stackoverflow.com/questions/44060315/reconnecting-a-websocket-in-angular-and-rxjs/44067972#44067972) +- [Rust JWT](https://github.com/Keats/jsonwebtoken) ## TODOs - Endpoints diff --git a/server/Cargo.lock b/server/Cargo.lock index b4557d009..21594ccf0 100644 --- a/server/Cargo.lock +++ b/server/Cargo.lock @@ -703,6 +703,20 @@ name = "itoa" version = "0.4.3" source = "registry+https://github.com/rust-lang/crates.io-index" +[[package]] +name = "jsonwebtoken" +version = "5.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "base64 0.9.3 (registry+https://github.com/rust-lang/crates.io-index)", + "chrono 0.4.6 (registry+https://github.com/rust-lang/crates.io-index)", + "ring 0.13.5 (registry+https://github.com/rust-lang/crates.io-index)", + "serde 1.0.88 (registry+https://github.com/rust-lang/crates.io-index)", + "serde_derive 1.0.88 (registry+https://github.com/rust-lang/crates.io-index)", + "serde_json 1.0.38 (registry+https://github.com/rust-lang/crates.io-index)", + "untrusted 0.6.2 (registry+https://github.com/rust-lang/crates.io-index)", +] + [[package]] name = "kernel32-sys" version = "0.2.2" @@ -1309,7 +1323,9 @@ dependencies = [ "dotenv 0.9.0 (registry+https://github.com/rust-lang/crates.io-index)", "env_logger 0.6.0 (registry+https://github.com/rust-lang/crates.io-index)", "failure 0.1.5 (registry+https://github.com/rust-lang/crates.io-index)", + "jsonwebtoken 5.0.1 (registry+https://github.com/rust-lang/crates.io-index)", "rand 0.6.5 (registry+https://github.com/rust-lang/crates.io-index)", + "regex 1.1.2 (registry+https://github.com/rust-lang/crates.io-index)", "serde 1.0.88 (registry+https://github.com/rust-lang/crates.io-index)", "serde_json 1.0.38 (registry+https://github.com/rust-lang/crates.io-index)", "strum 0.14.0 (registry+https://github.com/rust-lang/crates.io-index)", @@ -1977,6 +1993,7 @@ dependencies = [ "checksum iovec 0.1.2 (registry+https://github.com/rust-lang/crates.io-index)" = "dbe6e417e7d0975db6512b90796e8ce223145ac4e33c377e4a42882a0e88bb08" "checksum ipconfig 0.1.9 (registry+https://github.com/rust-lang/crates.io-index)" = "08f7eadeaf4b52700de180d147c4805f199854600b36faa963d91114827b2ffc" "checksum itoa 0.4.3 (registry+https://github.com/rust-lang/crates.io-index)" = "1306f3464951f30e30d12373d31c79fbd52d236e5e896fd92f96ec7babbbe60b" +"checksum jsonwebtoken 5.0.1 (registry+https://github.com/rust-lang/crates.io-index)" = "8d438ea707d465c230305963b67f8357a1d56fcfad9434797d7cb1c46c2e41df" "checksum kernel32-sys 0.2.2 (registry+https://github.com/rust-lang/crates.io-index)" = "7507624b29483431c0ba2d82aece8ca6cdba9382bff4ddd0f7490560c056098d" "checksum language-tags 0.2.2 (registry+https://github.com/rust-lang/crates.io-index)" = "a91d884b6667cd606bb5a69aa0c99ba811a115fc68915e7056ec08a46e93199a" "checksum lazy_static 1.3.0 (registry+https://github.com/rust-lang/crates.io-index)" = "bc5729f27f159ddd61f4df6228e827e86643d4d3e7c32183cb30a1c08f604a14" diff --git a/server/Cargo.toml b/server/Cargo.toml index 3c875e900..ebd7b568a 100644 --- a/server/Cargo.toml +++ b/server/Cargo.toml @@ -18,3 +18,5 @@ env_logger = "*" rand = "0.6.5" strum = "0.14.0" strum_macros = "0.14.0" +jsonwebtoken = "*" +regex = "1" diff --git a/server/migrations/2019-02-26-002946_create_user/up.sql b/server/migrations/2019-02-26-002946_create_user/up.sql index 577ff136a..d4edb3708 100644 --- a/server/migrations/2019-02-26-002946_create_user/up.sql +++ b/server/migrations/2019-02-26-002946_create_user/up.sql @@ -1,9 +1,9 @@ create table user_ ( id serial primary key, - name varchar(20) not null, + name varchar(20) not null unique, preferred_username varchar(20), password_encrypted text not null, - email text, + email text unique, icon bytea, published timestamp not null default now(), updated timestamp diff --git a/server/migrations/2019-02-27-170003_create_community/up.sql b/server/migrations/2019-02-27-170003_create_community/up.sql index 30deec5b8..1ee2e51df 100644 --- a/server/migrations/2019-02-27-170003_create_community/up.sql +++ b/server/migrations/2019-02-27-170003_create_community/up.sql @@ -1,6 +1,6 @@ create table community ( id serial primary key, - name varchar(20) not null, + name varchar(20) not null unique, published timestamp not null default now(), updated timestamp ); diff --git a/server/src/actions/comment.rs b/server/src/actions/comment.rs index d23382c6d..98d5322c5 100644 --- a/server/src/actions/comment.rs +++ b/server/src/actions/comment.rs @@ -23,14 +23,14 @@ pub struct Comment { pub updated: Option } -#[derive(Insertable, AsChangeset, Clone, Copy)] +#[derive(Insertable, AsChangeset, Clone)] #[table_name="comment"] -pub struct CommentForm<'a> { - pub content: &'a str, - pub attributed_to: &'a str, - pub post_id: &'a i32, - pub parent_id: Option<&'a i32>, - pub updated: Option<&'a chrono::NaiveDateTime> +pub struct CommentForm { + pub content: String, + pub attributed_to: String, + pub post_id: i32, + pub parent_id: Option, + pub updated: Option } #[derive(Identifiable, Queryable, Associations, PartialEq, Debug)] @@ -44,59 +44,55 @@ pub struct CommentLike { pub published: chrono::NaiveDateTime, } -#[derive(Insertable, AsChangeset, Clone, Copy)] +#[derive(Insertable, AsChangeset, Clone)] #[table_name="comment_like"] -pub struct CommentLikeForm<'a> { - pub comment_id: &'a i32, - pub fedi_user_id: &'a str, - pub score: &'a i16 +pub struct CommentLikeForm { + pub comment_id: i32, + pub fedi_user_id: String, + pub score: i16 } -impl<'a> Crud> for Comment { - fn read(conn: &PgConnection, comment_id: i32) -> Comment { +impl Crud for Comment { + fn read(conn: &PgConnection, comment_id: i32) -> Result { use schema::comment::dsl::*; comment.find(comment_id) - .first::(conn) - .expect("Error in query") + .first::(conn) } - fn delete(conn: &PgConnection, comment_id: i32) -> usize { + fn delete(conn: &PgConnection, comment_id: i32) -> Result { use schema::comment::dsl::*; diesel::delete(comment.find(comment_id)) .execute(conn) - .expect("Error deleting.") } - fn create(conn: &PgConnection, comment_form: CommentForm) -> Result { + fn create(conn: &PgConnection, comment_form: &CommentForm) -> Result { use schema::comment::dsl::*; insert_into(comment) .values(comment_form) - .get_result::(conn) + .get_result::(conn) } - fn update(conn: &PgConnection, comment_id: i32, comment_form: CommentForm) -> Comment { + fn update(conn: &PgConnection, comment_id: i32, comment_form: &CommentForm) -> Result { use schema::comment::dsl::*; diesel::update(comment.find(comment_id)) .set(comment_form) - .get_result::(conn) - .expect(&format!("Unable to find {}", comment_id)) + .get_result::(conn) } } -impl<'a> Likeable > for CommentLike { - fn like(conn: &PgConnection, comment_like_form: CommentLikeForm) -> Result { +impl Likeable for CommentLike { + fn like(conn: &PgConnection, comment_like_form: &CommentLikeForm) -> Result { use schema::comment_like::dsl::*; insert_into(comment_like) .values(comment_like_form) - .get_result::(conn) + .get_result::(conn) } - fn remove(conn: &PgConnection, comment_like_form: CommentLikeForm) -> usize { + fn remove(conn: &PgConnection, comment_like_form: &CommentLikeForm) -> Result { use schema::comment_like::dsl::*; diesel::delete(comment_like .filter(comment_id.eq(comment_like_form.comment_id)) - .filter(fedi_user_id.eq(comment_like_form.fedi_user_id))) + .filter(fedi_user_id.eq(&comment_like_form.fedi_user_id))) .execute(conn) - .expect("Error deleting.") } } @@ -117,17 +113,17 @@ mod tests { updated: None }; - let inserted_post = Post::create(&conn, new_post).unwrap(); + let inserted_post = Post::create(&conn, &new_post).unwrap(); let comment_form = CommentForm { content: "A test comment".into(), attributed_to: "test_user.com".into(), - post_id: &inserted_post.id, + post_id: inserted_post.id, parent_id: None, updated: None }; - let inserted_comment = Comment::create(&conn, comment_form).unwrap(); + let inserted_comment = Comment::create(&conn, &comment_form).unwrap(); let expected_comment = Comment { id: inserted_comment.id, @@ -142,20 +138,20 @@ mod tests { let child_comment_form = CommentForm { content: "A child comment".into(), attributed_to: "test_user.com".into(), - post_id: &inserted_post.id, - parent_id: Some(&inserted_comment.id), + post_id: inserted_post.id, + parent_id: Some(inserted_comment.id), updated: None }; - let inserted_child_comment = Comment::create(&conn, child_comment_form).unwrap(); + let inserted_child_comment = Comment::create(&conn, &child_comment_form).unwrap(); let comment_like_form = CommentLikeForm { - comment_id: &inserted_comment.id, + comment_id: inserted_comment.id, fedi_user_id: "test".into(), - score: &1 + score: 1 }; - let inserted_comment_like = CommentLike::like(&conn, comment_like_form).unwrap(); + let inserted_comment_like = CommentLike::like(&conn, &comment_like_form).unwrap(); let expected_comment_like = CommentLike { id: inserted_comment_like.id, @@ -165,12 +161,12 @@ mod tests { score: 1 }; - let read_comment = Comment::read(&conn, inserted_comment.id); - let updated_comment = Comment::update(&conn, inserted_comment.id, comment_form); - let like_removed = CommentLike::remove(&conn, comment_like_form); - let num_deleted = Comment::delete(&conn, inserted_comment.id); - Comment::delete(&conn, inserted_child_comment.id); - Post::delete(&conn, inserted_post.id); + let read_comment = Comment::read(&conn, inserted_comment.id).unwrap(); + let updated_comment = Comment::update(&conn, inserted_comment.id, &comment_form).unwrap(); + let like_removed = CommentLike::remove(&conn, &comment_like_form).unwrap(); + let num_deleted = Comment::delete(&conn, inserted_comment.id).unwrap(); + Comment::delete(&conn, inserted_child_comment.id).unwrap(); + Post::delete(&conn, inserted_post.id).unwrap(); assert_eq!(expected_comment, read_comment); assert_eq!(expected_comment, inserted_comment); diff --git a/server/src/actions/community.rs b/server/src/actions/community.rs index 03490369c..44d7b749c 100644 --- a/server/src/actions/community.rs +++ b/server/src/actions/community.rs @@ -2,9 +2,10 @@ extern crate diesel; use schema::{community, community_user, community_follower}; use diesel::*; use diesel::result::Error; +use serde::{Deserialize, Serialize}; use {Crud, Followable, Joinable}; -#[derive(Queryable, Identifiable, PartialEq, Debug)] +#[derive(Queryable, Identifiable, PartialEq, Debug, Serialize, Deserialize)] #[table_name="community"] pub struct Community { pub id: i32, @@ -13,11 +14,11 @@ pub struct Community { pub updated: Option } -#[derive(Insertable, AsChangeset, Clone, Copy)] +#[derive(Insertable, AsChangeset, Clone, Serialize, Deserialize)] #[table_name="community"] -pub struct CommunityForm<'a> { - pub name: &'a str, - pub updated: Option<&'a chrono::NaiveDateTime> +pub struct CommunityForm { + pub name: String, + pub updated: Option } #[derive(Identifiable, Queryable, Associations, PartialEq, Debug)] @@ -30,11 +31,11 @@ pub struct CommunityUser { pub published: chrono::NaiveDateTime, } -#[derive(Insertable, AsChangeset, Clone, Copy)] +#[derive(Insertable, AsChangeset, Clone)] #[table_name="community_user"] -pub struct CommunityUserForm<'a> { - pub community_id: &'a i32, - pub fedi_user_id: &'a str, +pub struct CommunityUserForm { + pub community_id: i32, + pub fedi_user_id: String, } #[derive(Identifiable, Queryable, Associations, PartialEq, Debug)] @@ -47,76 +48,72 @@ pub struct CommunityFollower { pub published: chrono::NaiveDateTime, } -#[derive(Insertable, AsChangeset, Clone, Copy)] +#[derive(Insertable, AsChangeset, Clone)] #[table_name="community_follower"] -pub struct CommunityFollowerForm<'a> { - pub community_id: &'a i32, - pub fedi_user_id: &'a str, +pub struct CommunityFollowerForm { + pub community_id: i32, + pub fedi_user_id: String, } -impl<'a> Crud> for Community { - fn read(conn: &PgConnection, community_id: i32) -> Community { +impl Crud for Community { + fn read(conn: &PgConnection, community_id: i32) -> Result { use schema::community::dsl::*; community.find(community_id) - .first::(conn) - .expect("Error in query") + .first::(conn) } - fn delete(conn: &PgConnection, community_id: i32) -> usize { + fn delete(conn: &PgConnection, community_id: i32) -> Result { use schema::community::dsl::*; diesel::delete(community.find(community_id)) .execute(conn) - .expect("Error deleting.") } - fn create(conn: &PgConnection, new_community: CommunityForm) -> Result { + fn create(conn: &PgConnection, new_community: &CommunityForm) -> Result { use schema::community::dsl::*; insert_into(community) .values(new_community) - .get_result::(conn) + .get_result::(conn) } - fn update(conn: &PgConnection, community_id: i32, new_community: CommunityForm) -> Community { + fn update(conn: &PgConnection, community_id: i32, new_community: &CommunityForm) -> Result { use schema::community::dsl::*; diesel::update(community.find(community_id)) .set(new_community) - .get_result::(conn) - .expect(&format!("Unable to find {}", community_id)) + .get_result::(conn) } } -impl<'a> Followable> for CommunityFollower { - fn follow(conn: &PgConnection, community_follower_form: CommunityFollowerForm) -> Result { +impl Followable for CommunityFollower { + fn follow(conn: &PgConnection, community_follower_form: &CommunityFollowerForm) -> Result { use schema::community_follower::dsl::*; insert_into(community_follower) .values(community_follower_form) - .get_result::(conn) + .get_result::(conn) } - fn ignore(conn: &PgConnection, community_follower_form: CommunityFollowerForm) -> usize { + fn ignore(conn: &PgConnection, community_follower_form: &CommunityFollowerForm) -> Result { use schema::community_follower::dsl::*; diesel::delete(community_follower - .filter(community_id.eq(community_follower_form.community_id)) - .filter(fedi_user_id.eq(community_follower_form.fedi_user_id))) + .filter(community_id.eq(&community_follower_form.community_id)) + .filter(fedi_user_id.eq(&community_follower_form.fedi_user_id))) .execute(conn) - .expect("Error deleting.") } } -impl<'a> Joinable> for CommunityUser { - fn join(conn: &PgConnection, community_user_form: CommunityUserForm) -> Result { +impl Joinable for CommunityUser { + fn join(conn: &PgConnection, community_user_form: &CommunityUserForm) -> Result { use schema::community_user::dsl::*; insert_into(community_user) .values(community_user_form) - .get_result::(conn) + .get_result::(conn) } - fn leave(conn: &PgConnection, community_user_form: CommunityUserForm) -> usize { + + fn leave(conn: &PgConnection, community_user_form: &CommunityUserForm) -> Result { use schema::community_user::dsl::*; diesel::delete(community_user .filter(community_id.eq(community_user_form.community_id)) - .filter(fedi_user_id.eq(community_user_form.fedi_user_id))) + .filter(fedi_user_id.eq(&community_user_form.fedi_user_id))) .execute(conn) - .expect("Error deleting.") } } @@ -135,7 +132,7 @@ mod tests { updated: None }; - let inserted_community = Community::create(&conn, new_community).unwrap(); + let inserted_community = Community::create(&conn, &new_community).unwrap(); let expected_community = Community { id: inserted_community.id, @@ -145,21 +142,21 @@ mod tests { }; let new_user = UserForm { - name: "thom".into(), + name: "terry".into(), preferred_username: None, password_encrypted: "nope".into(), email: None, updated: None }; - let inserted_user = User_::create(&conn, new_user).unwrap(); + let inserted_user = User_::create(&conn, &new_user).unwrap(); let community_follower_form = CommunityFollowerForm { - community_id: &inserted_community.id, + community_id: inserted_community.id, fedi_user_id: "test".into() }; - let inserted_community_follower = CommunityFollower::follow(&conn, community_follower_form).unwrap(); + let inserted_community_follower = CommunityFollower::follow(&conn, &community_follower_form).unwrap(); let expected_community_follower = CommunityFollower { id: inserted_community_follower.id, @@ -169,11 +166,11 @@ mod tests { }; let community_user_form = CommunityUserForm { - community_id: &inserted_community.id, + community_id: inserted_community.id, fedi_user_id: "test".into() }; - let inserted_community_user = CommunityUser::join(&conn, community_user_form).unwrap(); + let inserted_community_user = CommunityUser::join(&conn, &community_user_form).unwrap(); let expected_community_user = CommunityUser { id: inserted_community_user.id, @@ -182,12 +179,12 @@ mod tests { published: inserted_community_user.published }; - let read_community = Community::read(&conn, inserted_community.id); - let updated_community = Community::update(&conn, inserted_community.id, new_community); - let ignored_community = CommunityFollower::ignore(&conn, community_follower_form); - let left_community = CommunityUser::leave(&conn, community_user_form); - let num_deleted = Community::delete(&conn, inserted_community.id); - User_::delete(&conn, inserted_user.id); + let read_community = Community::read(&conn, inserted_community.id).unwrap(); + let updated_community = Community::update(&conn, inserted_community.id, &new_community).unwrap(); + let ignored_community = CommunityFollower::ignore(&conn, &community_follower_form).unwrap(); + let left_community = CommunityUser::leave(&conn, &community_user_form).unwrap(); + let num_deleted = Community::delete(&conn, inserted_community.id).unwrap(); + User_::delete(&conn, inserted_user.id).unwrap(); assert_eq!(expected_community, read_community); assert_eq!(expected_community, inserted_community); diff --git a/server/src/actions/post.rs b/server/src/actions/post.rs index dd80f582d..889fcf037 100644 --- a/server/src/actions/post.rs +++ b/server/src/actions/post.rs @@ -15,13 +15,13 @@ pub struct Post { pub updated: Option } -#[derive(Insertable, AsChangeset, Clone, Copy)] +#[derive(Insertable, AsChangeset, Clone)] #[table_name="post"] -pub struct PostForm<'a> { - pub name: &'a str, - pub url: &'a str, - pub attributed_to: &'a str, - pub updated: Option<&'a chrono::NaiveDateTime> +pub struct PostForm { + pub name: String, + pub url: String, + pub attributed_to: String, + pub updated: Option } #[derive(Identifiable, Queryable, Associations, PartialEq, Debug)] @@ -35,59 +35,55 @@ pub struct PostLike { pub published: chrono::NaiveDateTime, } -#[derive(Insertable, AsChangeset, Clone, Copy)] +#[derive(Insertable, AsChangeset, Clone)] #[table_name="post_like"] -pub struct PostLikeForm<'a> { - pub post_id: &'a i32, - pub fedi_user_id: &'a str, - pub score: &'a i16 +pub struct PostLikeForm { + pub post_id: i32, + pub fedi_user_id: String, + pub score: i16 } -impl<'a> Crud> for Post { - fn read(conn: &PgConnection, post_id: i32) -> Post { +impl Crud for Post { + fn read(conn: &PgConnection, post_id: i32) -> Result { use schema::post::dsl::*; post.find(post_id) - .first::(conn) - .expect("Error in query") + .first::(conn) } - fn delete(conn: &PgConnection, post_id: i32) -> usize { + fn delete(conn: &PgConnection, post_id: i32) -> Result { use schema::post::dsl::*; diesel::delete(post.find(post_id)) .execute(conn) - .expect("Error deleting.") } - fn create(conn: &PgConnection, new_post: PostForm) -> Result { + fn create(conn: &PgConnection, new_post: &PostForm) -> Result { use schema::post::dsl::*; insert_into(post) .values(new_post) - .get_result::(conn) + .get_result::(conn) } - fn update(conn: &PgConnection, post_id: i32, new_post: PostForm) -> Post { + fn update(conn: &PgConnection, post_id: i32, new_post: &PostForm) -> Result { use schema::post::dsl::*; diesel::update(post.find(post_id)) .set(new_post) - .get_result::(conn) - .expect(&format!("Unable to find {}", post_id)) + .get_result::(conn) } } -impl<'a> Likeable > for PostLike { - fn like(conn: &PgConnection, post_like_form: PostLikeForm) -> Result { +impl Likeable for PostLike { + fn like(conn: &PgConnection, post_like_form: &PostLikeForm) -> Result { use schema::post_like::dsl::*; insert_into(post_like) .values(post_like_form) - .get_result::(conn) + .get_result::(conn) } - fn remove(conn: &PgConnection, post_like_form: PostLikeForm) -> usize { + fn remove(conn: &PgConnection, post_like_form: &PostLikeForm) -> Result { use schema::post_like::dsl::*; diesel::delete(post_like .filter(post_id.eq(post_like_form.post_id)) - .filter(fedi_user_id.eq(post_like_form.fedi_user_id))) + .filter(fedi_user_id.eq(&post_like_form.fedi_user_id))) .execute(conn) - .expect("Error deleting.") } } @@ -107,7 +103,7 @@ mod tests { updated: None }; - let inserted_post = Post::create(&conn, new_post).unwrap(); + let inserted_post = Post::create(&conn, &new_post).unwrap(); let expected_post = Post { id: inserted_post.id, @@ -119,12 +115,12 @@ mod tests { }; let post_like_form = PostLikeForm { - post_id: &inserted_post.id, + post_id: inserted_post.id, fedi_user_id: "test".into(), - score: &1 + score: 1 }; - let inserted_post_like = PostLike::like(&conn, post_like_form).unwrap(); + let inserted_post_like = PostLike::like(&conn, &post_like_form).unwrap(); let expected_post_like = PostLike { id: inserted_post_like.id, @@ -134,10 +130,10 @@ mod tests { score: 1 }; - let read_post = Post::read(&conn, inserted_post.id); - let updated_post = Post::update(&conn, inserted_post.id, new_post); - let like_removed = PostLike::remove(&conn, post_like_form); - let num_deleted = Post::delete(&conn, inserted_post.id); + let read_post = Post::read(&conn, inserted_post.id).unwrap(); + let updated_post = Post::update(&conn, inserted_post.id, &new_post).unwrap(); + let like_removed = PostLike::remove(&conn, &post_like_form).unwrap(); + let num_deleted = Post::delete(&conn, inserted_post.id).unwrap(); assert_eq!(expected_post, read_post); assert_eq!(expected_post, inserted_post); diff --git a/server/src/actions/user.rs b/server/src/actions/user.rs index 8556525f4..6016580d6 100644 --- a/server/src/actions/user.rs +++ b/server/src/actions/user.rs @@ -1,9 +1,11 @@ -extern crate diesel; use schema::user_; use diesel::*; use diesel::result::Error; use schema::user_::dsl::*; -use Crud; +use serde::{Serialize, Deserialize}; +use {Crud,is_email_regex}; +use jsonwebtoken::{encode, decode, Header, Validation}; +use bcrypt::{DEFAULT_COST, hash}; #[derive(Queryable, Identifiable, PartialEq, Debug)] #[table_name="user_"] @@ -18,43 +20,75 @@ pub struct User_ { pub updated: Option } -#[derive(Insertable, AsChangeset, Clone, Copy)] +#[derive(Insertable, AsChangeset, Clone)] #[table_name="user_"] -pub struct UserForm<'a> { - pub name: &'a str, - pub preferred_username: Option<&'a str>, - pub password_encrypted: &'a str, - pub email: Option<&'a str>, - pub updated: Option<&'a chrono::NaiveDateTime> +pub struct UserForm { + pub name: String, + pub preferred_username: Option, + pub password_encrypted: String, + pub email: Option, + pub updated: Option } -impl<'a> Crud> for User_ { - fn read(conn: &PgConnection, user_id: i32) -> User_ { +impl Crud for User_ { + fn read(conn: &PgConnection, user_id: i32) -> Result { user_.find(user_id) - .first::(conn) - .expect("Error in query") + .first::(conn) } - fn delete(conn: &PgConnection, user_id: i32) -> usize { + fn delete(conn: &PgConnection, user_id: i32) -> Result { diesel::delete(user_.find(user_id)) .execute(conn) - .expect("Error deleting.") } - fn create(conn: &PgConnection, form: UserForm) -> Result { + fn create(conn: &PgConnection, form: &UserForm) -> Result { let mut edited_user = form.clone(); - // Add the rust crypt - edited_user.password_encrypted = "here"; - // edited_user.password_encrypted; - insert_into(user_) - .values(edited_user) - .get_result::(conn) + let password_hash = hash(&form.password_encrypted, DEFAULT_COST) + .expect("Couldn't hash password"); + edited_user.password_encrypted = password_hash; + insert_into(user_) + .values(edited_user) + .get_result::(conn) } - fn update(conn: &PgConnection, user_id: i32, form: UserForm) -> User_ { + fn update(conn: &PgConnection, user_id: i32, form: &UserForm) -> Result { let mut edited_user = form.clone(); - edited_user.password_encrypted = "here"; + let password_hash = hash(&form.password_encrypted, DEFAULT_COST) + .expect("Couldn't hash password"); + edited_user.password_encrypted = password_hash; diesel::update(user_.find(user_id)) .set(edited_user) - .get_result::(conn) - .expect(&format!("Unable to find user {}", user_id)) + .get_result::(conn) + } +} + +#[derive(Debug, Serialize, Deserialize)] +struct Claims { + id: i32, + username: String +} + +type Jwt = String; +impl User_ { + pub fn jwt(&self) -> Jwt { + let my_claims = Claims { + id: self.id, + username: self.name.to_owned() + }; + encode(&Header::default(), &my_claims, "secret".as_ref()).unwrap() + } + + pub fn find_by_email_or_username(conn: &PgConnection, username_or_email: &str) -> Result { + if is_email_regex(username_or_email) { + user_.filter(email.eq(username_or_email)) + .first::(conn) + } else { + user_.filter(name.eq(username_or_email)) + .first::(conn) + } + } + + pub fn find_by_jwt(conn: &PgConnection, jwt: &str) -> Result { + let token = decode::(&jwt, "secret".as_ref(), &Validation::default()) + .expect("Couldn't decode jwt"); + Self::read(&conn, token.claims.id) } } @@ -75,26 +109,26 @@ mod tests { updated: None }; - let inserted_user = User_::create(&conn, new_user).unwrap(); + let inserted_user = User_::create(&conn, &new_user).unwrap(); let expected_user = User_ { id: inserted_user.id, name: "thom".into(), preferred_username: None, - password_encrypted: "here".into(), + password_encrypted: "$2y$12$YXpNpYsdfjmed.QlYLvw4OfTCgyKUnKHc/V8Dgcf9YcVKHPaYXYYy".into(), email: None, icon: None, published: inserted_user.published, updated: None }; - let read_user = User_::read(&conn, inserted_user.id); - let updated_user = User_::update(&conn, inserted_user.id, new_user); - let num_deleted = User_::delete(&conn, inserted_user.id); + let read_user = User_::read(&conn, inserted_user.id).unwrap(); + let updated_user = User_::update(&conn, inserted_user.id, &new_user).unwrap(); + let num_deleted = User_::delete(&conn, inserted_user.id).unwrap(); - assert_eq!(expected_user, read_user); - assert_eq!(expected_user, inserted_user); - assert_eq!(expected_user, updated_user); + assert_eq!(expected_user.id, read_user.id); + assert_eq!(expected_user.id, inserted_user.id); + assert_eq!(expected_user.id, updated_user.id); assert_eq!(1, num_deleted); } } diff --git a/server/src/bin/main.rs b/server/src/bin/main.rs index 25181aaaf..bd4c2d212 100644 --- a/server/src/bin/main.rs +++ b/server/src/bin/main.rs @@ -4,17 +4,15 @@ use std::time::{Instant, Duration}; use server::actix::*; use server::actix_web::server::HttpServer; use server::actix_web::{fs, http, ws, App, Error, HttpRequest, HttpResponse}; +use std::str::FromStr; +use server::websocket_server::server::*; /// How often heartbeat pings are sent const HEARTBEAT_INTERVAL: Duration = Duration::from_secs(5); /// How long before lack of client response causes a timeout const CLIENT_TIMEOUT: Duration = Duration::from_secs(10); -use server::websocket_server::server::*; -use std::str::FromStr; -// use server::websocket_server::server::UserOperation::from_str; - /// This is our websocket route state, this state is shared with all route /// instances via `HttpContext::state()` struct WsChatSessionState { @@ -92,7 +90,7 @@ use server::serde_json::Value; /// WebSocket message handler impl StreamHandler for WSSession { fn handle(&mut self, msg: ws::Message, ctx: &mut Self::Context) { - // println!("WEBSOCKET MESSAGE: {:?}", msg); + println!("WEBSOCKET MESSAGE: {:?}", msg); match msg { ws::Message::Ping(msg) => { self.hb = Instant::now(); @@ -108,7 +106,7 @@ impl StreamHandler for WSSession { // Get the OP command, and its data let op: &str = &json["op"].as_str().unwrap(); let data: &Value = &json["data"]; - + let user_operation: UserOperation = UserOperation::from_str(op).unwrap(); match user_operation { @@ -116,7 +114,23 @@ impl StreamHandler for WSSession { let login: Login = serde_json::from_str(&data.to_string()).unwrap(); ctx.state() .addr - .do_send(login); + .send(login) + .into_actor(self) + .then(|res, _, ctx| { + match res { + Ok(response) => match response { + Ok(t) => ctx.text(serde_json::to_string(&t).unwrap()), + Err(e) => { + let error_message_str: String = serde_json::to_string(&e).unwrap(); + eprintln!("{}", &error_message_str); + ctx.text(&error_message_str); + } + }, + _ => println!("Something is wrong"), + } + fut::ok(()) + }) + .wait(ctx) }, UserOperation::Register => { let register: Register = serde_json::from_str(&data.to_string()).unwrap(); @@ -126,13 +140,44 @@ impl StreamHandler for WSSession { .into_actor(self) .then(|res, _, ctx| { match res { - Ok(wut) => ctx.text(wut), + Ok(response) => match response { + Ok(t) => ctx.text(serde_json::to_string(&t).unwrap()), + Err(e) => { + let error_message_str: String = serde_json::to_string(&e).unwrap(); + eprintln!("{}", &error_message_str); + ctx.text(&error_message_str); + } + }, _ => println!("Something is wrong"), } fut::ok(()) }) .wait(ctx) - } + }, + UserOperation::CreateCommunity => { + use server::actions::community::CommunityForm; + let auth: &str = &json["auth"].as_str().unwrap(); + let community_form: CommunityForm = serde_json::from_str(&data.to_string()).unwrap(); + ctx.state() + .addr + .send(community_form) + .into_actor(self) + .then(|res, _, ctx| { + match res { + Ok(response) => match response { + Ok(t) => ctx.text(serde_json::to_string(&t).unwrap()), + Err(e) => { + let error_message_str: String = serde_json::to_string(&e).unwrap(); + eprintln!("{}", &error_message_str); + ctx.text(&error_message_str); + } + }, + _ => println!("Something is wrong"), + } + fut::ok(()) + }) + .wait(ctx) + }, _ => ctx.text(format!("!!! unknown command: {:?}", m)), } diff --git a/server/src/lib.rs b/server/src/lib.rs index 3daeb8d2d..fcc9c2c83 100644 --- a/server/src/lib.rs +++ b/server/src/lib.rs @@ -8,6 +8,9 @@ pub extern crate actix; pub extern crate actix_web; pub extern crate rand; pub extern crate strum; +pub extern crate jsonwebtoken; +pub extern crate bcrypt; +pub extern crate regex; #[macro_use] pub extern crate strum_macros; pub mod schema; @@ -20,28 +23,28 @@ use diesel::pg::PgConnection; use diesel::result::Error; use dotenv::dotenv; use std::env; - +use regex::Regex; pub trait Crud { - fn create(conn: &PgConnection, form: T) -> Result where Self: Sized; - fn read(conn: &PgConnection, id: i32) -> Self; - fn update(conn: &PgConnection, id: i32, form: T) -> Self; - fn delete(conn: &PgConnection, id: i32) -> usize; + fn create(conn: &PgConnection, form: &T) -> Result where Self: Sized; + fn read(conn: &PgConnection, id: i32) -> Result where Self: Sized; + fn update(conn: &PgConnection, id: i32, form: &T) -> Result where Self: Sized; + fn delete(conn: &PgConnection, id: i32) -> Result where Self: Sized; } pub trait Followable { - fn follow(conn: &PgConnection, form: T) -> Result where Self: Sized; - fn ignore(conn: &PgConnection, form: T) -> usize; + fn follow(conn: &PgConnection, form: &T) -> Result where Self: Sized; + fn ignore(conn: &PgConnection, form: &T) -> Result where Self: Sized; } pub trait Joinable { - fn join(conn: &PgConnection, form: T) -> Result where Self: Sized; - fn leave(conn: &PgConnection, form: T) -> usize; + fn join(conn: &PgConnection, form: &T) -> Result where Self: Sized; + fn leave(conn: &PgConnection, form: &T) -> Result where Self: Sized; } pub trait Likeable { - fn like(conn: &PgConnection, form: T) -> Result where Self: Sized; - fn remove(conn: &PgConnection, form: T) -> usize; + fn like(conn: &PgConnection, form: &T) -> Result where Self: Sized; + fn remove(conn: &PgConnection, form: &T) -> Result where Self: Sized; } pub fn establish_connection() -> PgConnection { @@ -61,7 +64,7 @@ impl Settings { Settings { db_url: env::var("DATABASE_URL") .expect("DATABASE_URL must be set"), - hostname: env::var("HOSTNAME").unwrap_or("http://0.0.0.0".to_string()) + hostname: env::var("HOSTNAME").unwrap_or("http://0.0.0.0".to_string()) } } fn api_endpoint(&self) -> String { @@ -78,11 +81,22 @@ pub fn naive_now() -> NaiveDateTime { chrono::prelude::Utc::now().naive_utc() } +pub fn is_email_regex(test: &str) -> bool { + let re = Regex::new(r"^[a-zA-Z0-9.!#$%&’*+/=?^_`{|}~-]+@[a-zA-Z0-9-]+(?:\.[a-zA-Z0-9-]+)*$").unwrap(); + re.is_match(test) +} + #[cfg(test)] mod tests { - use Settings; - #[test] + use {Settings, is_email_regex}; + #[test] fn test_api() { assert_eq!(Settings::get().api_endpoint(), "http://0.0.0.0/api/v1"); } -} + + #[test] + fn test_email() { + assert!(is_email_regex("gush@gmail.com")); + assert!(!is_email_regex("nada_neutho")); + } +} diff --git a/server/src/websocket_server/server.rs b/server/src/websocket_server/server.rs index 2d410176d..857db3066 100644 --- a/server/src/websocket_server/server.rs +++ b/server/src/websocket_server/server.rs @@ -6,19 +6,27 @@ use actix::prelude::*; use rand::{rngs::ThreadRng, Rng}; use std::collections::{HashMap, HashSet}; use serde::{Deserialize, Serialize}; +use bcrypt::{verify}; use {Crud,establish_connection}; +use actions::community::*; #[derive(EnumString,ToString,Debug)] pub enum UserOperation { - Login, Register, Logout, Join, Edit, Reply, Vote, Delete, NextPage, Sticky -} - -pub enum MessageType { - Comments, Users, Ping, Pong + Login, Register, Logout, CreateCommunity, Join, Edit, Reply, Vote, Delete, NextPage, Sticky } +#[derive(EnumString,ToString,Debug)] +pub enum MessageToUser { + Comments, Users, Ping, Pong, Error +} + +#[derive(Serialize, Deserialize)] +pub struct ErrorMessage { + op: String, + error: String +} /// Chat server sends this messages to session #[derive(Message)] @@ -66,14 +74,16 @@ pub struct Join { pub name: String, } -#[derive(Message)] #[derive(Serialize, Deserialize)] pub struct Login { - pub username: String, + pub username_or_email: String, pub password: String } -// #[derive(Message)] +impl actix::Message for Login { + type Result = Result; +} + #[derive(Serialize, Deserialize)] pub struct Register { username: String, @@ -82,9 +92,31 @@ pub struct Register { password_verify: String } -impl actix::Message for Register { - type Result = String; +#[derive(Serialize, Deserialize)] +pub struct LoginResponse { + op: String, + jwt: String } + +impl actix::Message for Register { + type Result = Result; +} + +// #[derive(Serialize, Deserialize)] +// pub struct CreateCommunity { +// name: String +// } + +#[derive(Serialize, Deserialize)] +pub struct CreateCommunityResponse { + op: String, + community: Community +} + +impl actix::Message for CommunityForm { + type Result = Result; +} + /// `ChatServer` manages chat rooms and responsible for coordinating chat /// session. implementation is super primitive pub struct ChatServer { @@ -233,10 +265,47 @@ impl Handler for ChatServer { impl Handler for ChatServer { - type Result = (); - fn handle(&mut self, msg: Login, _: &mut Context) { - println!("{}", msg.password); + type Result = MessageResult; + fn handle(&mut self, msg: Login, _: &mut Context) -> Self::Result { + use actions::user::*; + let conn = establish_connection(); + + // Fetch that username / email + let user: User_ = match User_::find_by_email_or_username(&conn, &msg.username_or_email) { + Ok(user) => user, + Err(e) => return MessageResult( + Err( + ErrorMessage { + op: UserOperation::Login.to_string(), + error: "Couldn't find that username or email".to_string() + } + ) + ) + }; + + // Verify the password + let valid: bool = verify(&msg.password, &user.password_encrypted).unwrap_or(false); + if !valid { + return MessageResult( + Err( + ErrorMessage { + op: UserOperation::Login.to_string(), + error: "Password incorrect".to_string() + } + ) + ) + } + + // Return the jwt + MessageResult( + Ok( + LoginResponse { + op: UserOperation::Login.to_string(), + jwt: user.jwt() + } + ) + ) } } @@ -248,22 +317,79 @@ impl Handler for ChatServer { use actions::user::*; let conn = establish_connection(); - // TODO figure out how to return values, and throw errors + // Make sure passwords match + if msg.password != msg.password_verify { + return MessageResult( + Err( + ErrorMessage { + op: UserOperation::Register.to_string(), + error: "Passwords do not match.".to_string() + } + ) + ); + } // Register the new user let user_form = UserForm { - name: &msg.username, - email: msg.email.as_ref().map(|x| &**x), - password_encrypted: &msg.password, + name: msg.username, + email: msg.email, + password_encrypted: msg.password, preferred_username: None, updated: None }; - let inserted_user = User_::create(&conn, user_form).unwrap(); + // Create the user + let inserted_user = match User_::create(&conn, &user_form) { + Ok(user) => user, + Err(e) => return MessageResult( + Err( + ErrorMessage { + op: UserOperation::Register.to_string(), + error: "User already exists.".to_string() // overwrite the diesel error + } + ) + ) + }; - // Return the jwt - MessageResult("hi".to_string()) + MessageResult( + Ok( + LoginResponse { + op: UserOperation::Register.to_string(), + jwt: inserted_user.jwt() + } + ) + ) } } + + +impl Handler for ChatServer { + + type Result = MessageResult; + + fn handle(&mut self, form: CommunityForm, _: &mut Context) -> Self::Result { + let conn = establish_connection(); + let community = match Community::create(&conn, &form) { + Ok(community) => community, + Err(e) => return MessageResult( + Err( + ErrorMessage { + op: UserOperation::CreateCommunity.to_string(), + error: "Community already exists.".to_string() // overwrite the diesel error + } + ) + ) + }; + + MessageResult( + Ok( + CreateCommunityResponse { + op: UserOperation::CreateCommunity.to_string(), + community: community + } + ) + ) + } +} diff --git a/ui/package.json b/ui/package.json index ca4fa368e..08443b14b 100644 --- a/ui/package.json +++ b/ui/package.json @@ -15,11 +15,15 @@ }, "engineStrict": true, "dependencies": { + "@types/js-cookie": "^2.2.1", "classcat": "^1.1.3", "dotenv": "^6.1.0", "inferno": "^7.0.1", "inferno-router": "^7.0.1", - "moment": "^2.22.2" + "js-cookie": "^2.2.0", + "jwt-decode": "^2.2.0", + "moment": "^2.22.2", + "rxjs": "^6.4.0" }, "devDependencies": { "fuse-box": "3.1.3", diff --git a/ui/src/components/create-community.tsx b/ui/src/components/create-community.tsx new file mode 100644 index 000000000..dbacd18df --- /dev/null +++ b/ui/src/components/create-community.tsx @@ -0,0 +1,90 @@ +import { Component, linkEvent } from 'inferno'; +import { Subscription } from "rxjs"; +import { retryWhen, delay, take } from 'rxjs/operators'; +import { CommunityForm, UserOperation } from '../interfaces'; +import { WebSocketService, UserService } from '../services'; +import { msgOp } from '../utils'; + +interface State { + communityForm: CommunityForm; +} + +let emptyState: State = { + communityForm: { + name: null, + } +} + +export class CreateCommunity extends Component { + private subscription: Subscription; + + constructor(props, context) { + super(props, context); + + this.state = emptyState; + + this.subscription = WebSocketService.Instance.subject + .pipe(retryWhen(errors => errors.pipe(delay(3000), take(10)))) + .subscribe( + (msg) => this.parseMessage(msg), + (err) => console.error(err), + ); + } + + componentWillUnmount() { + this.subscription.unsubscribe(); + } + + render() { + return ( +
+
+
+ {this.communityForm()} +
+
+
+ ) + } + + communityForm() { + return ( +
+
+

Create Forum

+
+ +
+ +
+
+
+
+ +
+
+
+
+ ); + } + + handleCreateCommunitySubmit(i: CreateCommunity, event) { + event.preventDefault(); + WebSocketService.Instance.createCommunity(i.state.communityForm); + } + + handleCommunityNameChange(i: CreateCommunity, event) { + i.state.communityForm.name = event.target.value; + i.setState(i.state); + } + + parseMessage(msg: any) { + let op: UserOperation = msgOp(msg); + if (msg.error) { + alert(msg.error); + return; + } else { + } + } + +} diff --git a/ui/src/components/create-post.tsx b/ui/src/components/create-post.tsx new file mode 100644 index 000000000..bb6e60e27 --- /dev/null +++ b/ui/src/components/create-post.tsx @@ -0,0 +1,57 @@ +import { Component, linkEvent } from 'inferno'; + +import { LoginForm, PostForm, UserOperation } from '../interfaces'; +import { WebSocketService, UserService } from '../services'; +import { msgOp } from '../utils'; + +interface State { + postForm: PostForm; +} + +let emptyState: State = { + postForm: { + name: null, + url: null, + attributed_to: null + } +} + +export class CreatePost extends Component { + + constructor(props, context) { + super(props, context); + + this.state = emptyState; + + WebSocketService.Instance.subject.subscribe( + (msg) => this.parseMessage(msg), + (err) => console.error(err), + () => console.log('complete') + ); + } + + + render() { + return ( +
+
+
+ create post + {/* {this.postForm()} */} +
+
+
+ ) + } + + parseMessage(msg: any) { + console.log(msg); + let op: UserOperation = msgOp(msg); + if (msg.error) { + alert(msg.error); + return; + } else { + } + } + +} diff --git a/ui/src/components/login.tsx b/ui/src/components/login.tsx index fd6f5045c..372b15574 100644 --- a/ui/src/components/login.tsx +++ b/ui/src/components/login.tsx @@ -1,7 +1,9 @@ import { Component, linkEvent } from 'inferno'; - -import { LoginForm, RegisterForm } from '../interfaces'; -import { WebSocketService } from '../services'; +import { Subscription } from "rxjs"; +import { retryWhen, delay, take } from 'rxjs/operators'; +import { LoginForm, RegisterForm, UserOperation } from '../interfaces'; +import { WebSocketService, UserService } from '../services'; +import { msgOp } from '../utils'; interface State { loginForm: LoginForm; @@ -10,24 +12,36 @@ interface State { let emptyState: State = { loginForm: { - username: null, - password: null + username_or_email: undefined, + password: undefined }, registerForm: { - username: null, - password: null, - password_verify: null + username: undefined, + password: undefined, + password_verify: undefined } } export class Login extends Component { + private subscription: Subscription; constructor(props, context) { super(props, context); this.state = emptyState; + this.subscription = WebSocketService.Instance.subject + .pipe(retryWhen(errors => errors.pipe(delay(3000), take(10)))) + .subscribe( + (msg) => this.parseMessage(msg), + (err) => console.error(err), + ); } + + componentWillUnmount() { + this.subscription.unsubscribe(); + } + render() { return (
@@ -51,7 +65,7 @@ export class Login extends Component {
- +
@@ -108,38 +122,55 @@ export class Login extends Component { } handleLoginSubmit(i: Login, event) { - console.log(i.state); event.preventDefault(); WebSocketService.Instance.login(i.state.loginForm); } handleLoginUsernameChange(i: Login, event) { - i.state.loginForm.username = event.target.value; + i.state.loginForm.username_or_email = event.target.value; + i.setState(i.state); } handleLoginPasswordChange(i: Login, event) { i.state.loginForm.password = event.target.value; + i.setState(i.state); } handleRegisterSubmit(i: Login, event) { - console.log(i.state); event.preventDefault(); WebSocketService.Instance.register(i.state.registerForm); } handleRegisterUsernameChange(i: Login, event) { i.state.registerForm.username = event.target.value; + i.setState(i.state); } handleRegisterEmailChange(i: Login, event) { i.state.registerForm.email = event.target.value; + i.setState(i.state); } handleRegisterPasswordChange(i: Login, event) { i.state.registerForm.password = event.target.value; + i.setState(i.state); } - + handleRegisterPasswordVerifyChange(i: Login, event) { i.state.registerForm.password_verify = event.target.value; + i.setState(i.state); + } + + parseMessage(msg: any) { + let op: UserOperation = msgOp(msg); + if (msg.error) { + alert(msg.error); + return; + } else { + if (op == UserOperation.Register || op == UserOperation.Login) { + UserService.Instance.login(msg.jwt); + this.props.history.push('/'); + } + } } } diff --git a/ui/src/components/navbar.tsx b/ui/src/components/navbar.tsx index 86d5d1d2e..4cf6d6d27 100644 --- a/ui/src/components/navbar.tsx +++ b/ui/src/components/navbar.tsx @@ -1,38 +1,62 @@ import { Component, linkEvent } from 'inferno'; import { Link } from 'inferno-router'; import { repoUrl } from '../utils'; +import { UserService } from '../services'; export class Navbar extends Component { constructor(props, context) { super(props, context); + this.state = {isLoggedIn: UserService.Instance.loggedIn}; + + // Subscribe to user changes + UserService.Instance.sub.subscribe(user => { + let loggedIn: boolean = user !== null; + this.setState({isLoggedIn: loggedIn}); + }); } render() { return ( -
{this.navbar()}
+
{this.navbar()}
) } // TODO class active corresponding to current page + // TODO toggle css collapse navbar() { return ( -