]> Untitled Git - lemmy.git/commitdiff
Adding login and Register
authorDessalines <tyhou13@gmx.com>
Sat, 23 Mar 2019 01:42:57 +0000 (18:42 -0700)
committerDessalines <tyhou13@gmx.com>
Sat, 23 Mar 2019 01:42:57 +0000 (18:42 -0700)
- Login and Register  mostly working.
- Starting to work on creating communities.

26 files changed:
README.md
server/Cargo.lock
server/Cargo.toml
server/migrations/2019-02-26-002946_create_user/up.sql
server/migrations/2019-02-27-170003_create_community/up.sql
server/src/actions/comment.rs
server/src/actions/community.rs
server/src/actions/post.rs
server/src/actions/user.rs
server/src/bin/main.rs
server/src/lib.rs
server/src/websocket_server/server.rs
ui/package.json
ui/src/components/create-community.tsx [new file with mode: 0644]
ui/src/components/create-post.tsx [new file with mode: 0644]
ui/src/components/login.tsx
ui/src/components/navbar.tsx
ui/src/index.tsx
ui/src/interfaces.ts
ui/src/main.css
ui/src/services.ts [deleted file]
ui/src/services/UserService.ts [new file with mode: 0644]
ui/src/services/WebSocketService.ts [new file with mode: 0644]
ui/src/services/index.ts [new file with mode: 0644]
ui/src/utils.ts
ui/yarn.lock

index b3f8d11b066cafa2b07651084fba7aed81f41e6c..ce8672e660400e7e38b1a4e5152ef5e8a5665a2d 100644 (file)
--- 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
index b4557d009f199a60b26a25ef395dfea5bd0b1bd1..21594ccf02491a7a7af3f4bacf8772a8367c87e6 100644 (file)
@@ -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"
index 3c875e9002597c0f433b45b193d7fbf5045ca699..ebd7b568af80b7e1072fba4a063a0c877f310aab 100644 (file)
@@ -18,3 +18,5 @@ env_logger = "*"
 rand = "0.6.5"
 strum = "0.14.0"
 strum_macros = "0.14.0"
+jsonwebtoken = "*"
+regex = "1"
index 577ff136ab34fc230fe2c002f23ba6979f7c4dc1..d4edb3708c6f9c6fa81310ad046a6808a8ace346 100644 (file)
@@ -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
index 30deec5b8e1e1b547c781e7d36eb943e5f07fecc..1ee2e51df19c6a0cd401f18bf00913a93f3bd79c 100644 (file)
@@ -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
 );
index d23382c6d95bb59065451f4dd1878c060f6a0ea0..98d5322c58b0a2032c0411dc8c3570f8d5fd3101 100644 (file)
@@ -23,14 +23,14 @@ pub struct Comment {
   pub updated: Option<chrono::NaiveDateTime>
 }
 
-#[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<i32>,
+  pub updated: Option<chrono::NaiveDateTime>
 }
 
 #[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<CommentForm<'a>> for Comment {
-  fn read(conn: &PgConnection, comment_id: i32) -> Comment {
+impl Crud<CommentForm> for Comment {
+  fn read(conn: &PgConnection, comment_id: i32) -> Result<Self, Error> {
     use schema::comment::dsl::*;
     comment.find(comment_id)
-      .first::<Comment>(conn)
-      .expect("Error in query")
+      .first::<Self>(conn)
   }
 
-  fn delete(conn: &PgConnection, comment_id: i32) -> usize {
+  fn delete(conn: &PgConnection, comment_id: i32) -> Result<usize, Error> {
     use schema::comment::dsl::*;
     diesel::delete(comment.find(comment_id))
       .execute(conn)
-      .expect("Error deleting.")
   }
 
-  fn create(conn: &PgConnection, comment_form: CommentForm) -> Result<Comment, Error> {
+  fn create(conn: &PgConnection, comment_form: &CommentForm) -> Result<Self, Error> {
     use schema::comment::dsl::*;
       insert_into(comment)
         .values(comment_form)
-        .get_result::<Comment>(conn)
+        .get_result::<Self>(conn)
   }
 
-  fn update(conn: &PgConnection, comment_id: i32, comment_form: CommentForm) -> Comment {
+  fn update(conn: &PgConnection, comment_id: i32, comment_form: &CommentForm) -> Result<Self, Error> {
     use schema::comment::dsl::*;
     diesel::update(comment.find(comment_id))
       .set(comment_form)
-      .get_result::<Comment>(conn)
-      .expect(&format!("Unable to find {}", comment_id))
+      .get_result::<Self>(conn)
   }
 }
 
-impl<'a> Likeable <CommentLikeForm<'a>> for CommentLike {
-  fn like(conn: &PgConnection, comment_like_form: CommentLikeForm) -> Result<CommentLike, Error> {
+impl Likeable <CommentLikeForm> for CommentLike {
+  fn like(conn: &PgConnection, comment_like_form: &CommentLikeForm) -> Result<Self, Error> {
     use schema::comment_like::dsl::*;
     insert_into(comment_like)
       .values(comment_like_form)
-      .get_result::<CommentLike>(conn)
+      .get_result::<Self>(conn)
   }
-  fn remove(conn: &PgConnection, comment_like_form: CommentLikeForm) -> usize {
+  fn remove(conn: &PgConnection, comment_like_form: &CommentLikeForm) -> Result<usize, Error> {
     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);
index 03490369c49d3dda82124ff0d7b8574d19b0727e..44d7b749c78abbefebfbb3695ea13dca610db68c 100644 (file)
@@ -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<chrono::NaiveDateTime>
 }
 
-#[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<chrono::NaiveDateTime>
 }
 
 #[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<CommunityForm<'a>> for Community {
-  fn read(conn: &PgConnection, community_id: i32) -> Community {
+impl Crud<CommunityForm> for Community {
+  fn read(conn: &PgConnection, community_id: i32) -> Result<Self, Error> {
     use schema::community::dsl::*;
     community.find(community_id)
-      .first::<Community>(conn)
-      .expect("Error in query")
+      .first::<Self>(conn)
   }
 
-  fn delete(conn: &PgConnection, community_id: i32) -> usize {
+  fn delete(conn: &PgConnection, community_id: i32) -> Result<usize, Error> {
     use schema::community::dsl::*;
     diesel::delete(community.find(community_id))
       .execute(conn)
-      .expect("Error deleting.")
   }
 
-  fn create(conn: &PgConnection, new_community: CommunityForm) -> Result<Community, Error> {
+  fn create(conn: &PgConnection, new_community: &CommunityForm) -> Result<Self, Error> {
     use schema::community::dsl::*;
       insert_into(community)
         .values(new_community)
-        .get_result::<Community>(conn)
+        .get_result::<Self>(conn)
   }
 
-  fn update(conn: &PgConnection, community_id: i32, new_community: CommunityForm) -> Community {
+  fn update(conn: &PgConnection, community_id: i32, new_community: &CommunityForm) -> Result<Self, Error> {
     use schema::community::dsl::*;
     diesel::update(community.find(community_id))
       .set(new_community)
-      .get_result::<Community>(conn)
-      .expect(&format!("Unable to find {}", community_id))
+      .get_result::<Self>(conn)
   }
 }
 
-impl<'a> Followable<CommunityFollowerForm<'a>> for CommunityFollower {
-  fn follow(conn: &PgConnection, community_follower_form: CommunityFollowerForm) -> Result<CommunityFollower, Error> {
+impl Followable<CommunityFollowerForm> for CommunityFollower {
+  fn follow(conn: &PgConnection, community_follower_form: &CommunityFollowerForm) -> Result<Self, Error> {
     use schema::community_follower::dsl::*;
     insert_into(community_follower)
       .values(community_follower_form)
-      .get_result::<CommunityFollower>(conn)
+      .get_result::<Self>(conn)
   }
-  fn ignore(conn: &PgConnection, community_follower_form: CommunityFollowerForm) -> usize {
+  fn ignore(conn: &PgConnection, community_follower_form: &CommunityFollowerForm) -> Result<usize, Error> {
     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<CommunityUserForm<'a>> for CommunityUser {
-  fn join(conn: &PgConnection, community_user_form: CommunityUserForm) -> Result<CommunityUser, Error> {
+impl Joinable<CommunityUserForm> for CommunityUser {
+  fn join(conn: &PgConnection, community_user_form: &CommunityUserForm) -> Result<Self, Error> {
     use schema::community_user::dsl::*;
     insert_into(community_user)
       .values(community_user_form)
-      .get_result::<CommunityUser>(conn)
+      .get_result::<Self>(conn)
   }
-  fn leave(conn: &PgConnection, community_user_form: CommunityUserForm) -> usize {
+
+  fn leave(conn: &PgConnection, community_user_form: &CommunityUserForm) -> Result<usize, Error> {
     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);
index dd80f582d18aaf5a37144710af0bdbee877d08e6..889fcf037e85378a4e9f980c47a0679813cd1bf7 100644 (file)
@@ -15,13 +15,13 @@ pub struct Post {
   pub updated: Option<chrono::NaiveDateTime>
 }
 
-#[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<chrono::NaiveDateTime>
 }
 
 #[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<PostForm<'a>> for Post {
-  fn read(conn: &PgConnection, post_id: i32) -> Post {
+impl Crud<PostForm> for Post {
+  fn read(conn: &PgConnection, post_id: i32) -> Result<Self, Error> {
     use schema::post::dsl::*;
     post.find(post_id)
-      .first::<Post>(conn)
-      .expect("Error in query")
+      .first::<Self>(conn)
   }
 
-  fn delete(conn: &PgConnection, post_id: i32) -> usize {
+  fn delete(conn: &PgConnection, post_id: i32) -> Result<usize, Error> {
     use schema::post::dsl::*;
     diesel::delete(post.find(post_id))
       .execute(conn)
-      .expect("Error deleting.")
   }
 
-  fn create(conn: &PgConnection, new_post: PostForm) -> Result<Post, Error> {
+  fn create(conn: &PgConnection, new_post: &PostForm) -> Result<Self, Error> {
     use schema::post::dsl::*;
       insert_into(post)
         .values(new_post)
-        .get_result::<Post>(conn)
+        .get_result::<Self>(conn)
   }
 
-  fn update(conn: &PgConnection, post_id: i32, new_post: PostForm) -> Post {
+  fn update(conn: &PgConnection, post_id: i32, new_post: &PostForm) -> Result<Self, Error> {
     use schema::post::dsl::*;
     diesel::update(post.find(post_id))
       .set(new_post)
-      .get_result::<Post>(conn)
-      .expect(&format!("Unable to find {}", post_id))
+      .get_result::<Self>(conn)
   }
 }
 
-impl<'a> Likeable <PostLikeForm<'a>> for PostLike {
-  fn like(conn: &PgConnection, post_like_form: PostLikeForm) -> Result<PostLike, Error> {
+impl Likeable <PostLikeForm> for PostLike {
+  fn like(conn: &PgConnection, post_like_form: &PostLikeForm) -> Result<Self, Error> {
     use schema::post_like::dsl::*;
     insert_into(post_like)
       .values(post_like_form)
-      .get_result::<PostLike>(conn)
+      .get_result::<Self>(conn)
   }
-  fn remove(conn: &PgConnection, post_like_form: PostLikeForm) -> usize {
+  fn remove(conn: &PgConnection, post_like_form: &PostLikeForm) -> Result<usize, Error> {
     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);
index 8556525f4b6e2c726a1d86048d03e697664b62ee..6016580d604273f6cf7984cb8e542ac6f23219d6 100644 (file)
@@ -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<chrono::NaiveDateTime>
 }
 
-#[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<String>,
+    pub password_encrypted: String,
+    pub email: Option<String>,
+    pub updated: Option<chrono::NaiveDateTime>
 }
 
-impl<'a> Crud<UserForm<'a>> for User_ {
-  fn read(conn: &PgConnection, user_id: i32) -> User_ {
+impl Crud<UserForm> for User_ {
+  fn read(conn: &PgConnection, user_id: i32) -> Result<Self, Error> {
     user_.find(user_id)
-      .first::<User_>(conn)
-      .expect("Error in query")
+      .first::<Self>(conn)
   }
-  fn delete(conn: &PgConnection, user_id: i32) -> usize {
+  fn delete(conn: &PgConnection, user_id: i32) -> Result<usize, Error> {
     diesel::delete(user_.find(user_id))
       .execute(conn)
-      .expect("Error deleting.")
   }
-  fn create(conn: &PgConnection, form: UserForm) -> Result<User_, Error> {
+  fn create(conn: &PgConnection, form: &UserForm) -> Result<Self, Error> {
     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::<User_>(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::<Self>(conn)
   }
-  fn update(conn: &PgConnection, user_id: i32, form: UserForm) -> User_ {
+  fn update(conn: &PgConnection, user_id: i32, form: &UserForm) -> Result<Self, Error> {
     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::<User_>(conn)
-      .expect(&format!("Unable to find user {}", user_id))
+      .get_result::<Self>(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<Self, Error> {
+    if is_email_regex(username_or_email) {
+      user_.filter(email.eq(username_or_email))
+        .first::<User_>(conn)
+    } else {
+      user_.filter(name.eq(username_or_email))
+        .first::<User_>(conn)
+    }
+  }
+
+  pub fn find_by_jwt(conn: &PgConnection, jwt: &str) -> Result<Self, Error> {
+    let token = decode::<Claims>(&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);
   }
 }
index 25181aaafcc8b26d707bcba0cf46b65921c5c45e..bd4c2d212edddd30e382b95df7d7eacb6cbcb31f 100644 (file)
@@ -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<ws::Message, ws::ProtocolError> 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<ws::Message, ws::ProtocolError> 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<ws::Message, ws::ProtocolError> 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<ws::Message, ws::ProtocolError> 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)),
         } 
 
index 3daeb8d2d7f80be34f2e31341bf9000958e06f6b..fcc9c2c83ebd9f9ec739eb81ce028216d65f3ef2 100644 (file)
@@ -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<T> {
-  fn create(conn: &PgConnection, form: T) -> Result<Self, Error> 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<Self, Error> where Self: Sized;
+  fn read(conn: &PgConnection, id: i32) -> Result<Self, Error> where Self: Sized;  
+  fn update(conn: &PgConnection, id: i32, form: &T) -> Result<Self, Error> where Self: Sized;  
+  fn delete(conn: &PgConnection, id: i32) -> Result<usize, Error> where Self: Sized;
 }
 
 pub trait Followable<T> {
-  fn follow(conn: &PgConnection, form: T) -> Result<Self, Error> where Self: Sized;
-  fn ignore(conn: &PgConnection, form: T) -> usize;
+  fn follow(conn: &PgConnection, form: &T) -> Result<Self, Error> where Self: Sized;
+  fn ignore(conn: &PgConnection, form: &T) -> Result<usize, Error> where Self: Sized;
 }
 
 pub trait Joinable<T> {
-  fn join(conn: &PgConnection, form: T) -> Result<Self, Error> where Self: Sized;
-  fn leave(conn: &PgConnection, form: T) -> usize;
+  fn join(conn: &PgConnection, form: &T) -> Result<Self, Error> where Self: Sized;
+  fn leave(conn: &PgConnection, form: &T) -> Result<usize, Error> where Self: Sized;
 }
 
 pub trait Likeable<T> {
-  fn like(conn: &PgConnection, form: T) -> Result<Self, Error> where Self: Sized;
-  fn remove(conn: &PgConnection, form: T) -> usize;
+  fn like(conn: &PgConnection, form: &T) -> Result<Self, Error> where Self: Sized;
+  fn remove(conn: &PgConnection, form: &T) -> Result<usize, Error> 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"));
+  } 
+}
index 2d410176d8fd828a9957898256bceca9ed432b6f..857db3066b1033f8359ab9978db40a6c6192efd7 100644 (file)
@@ -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
+  Login, Register, Logout, CreateCommunity, Join, Edit, Reply, Vote, Delete, NextPage, Sticky
 }
 
-pub enum MessageType {
-  Comments, Users, Ping, Pong
-}
 
+#[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<LoginResponse, ErrorMessage>;
+}
+
 #[derive(Serialize, Deserialize)]
 pub struct Register {
   username: String,
@@ -82,9 +92,31 @@ pub struct Register {
   password_verify: String
 }
 
+#[derive(Serialize, Deserialize)]
+pub struct LoginResponse {
+  op: String,
+  jwt: String
+}
+
 impl actix::Message for Register {
-  type Result = String;
+  type Result = Result<LoginResponse, ErrorMessage>;
+}
+
+// #[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<CreateCommunityResponse, ErrorMessage>;
+}
+
 /// `ChatServer` manages chat rooms and responsible for coordinating chat
 /// session. implementation is super primitive
 pub struct ChatServer {
@@ -233,10 +265,47 @@ impl Handler<Join> for ChatServer {
 
 impl Handler<Login> for ChatServer {
 
-  type Result = ();
-  fn handle(&mut self, msg: Login, _: &mut Context<Self>) {
-    println!("{}", msg.password);
+  type Result = MessageResult<Login>;
+  fn handle(&mut self, msg: Login, _: &mut Context<Self>) -> 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<Register> 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<CommunityForm> for ChatServer {
+
+  type Result = MessageResult<CommunityForm>;
+
+  fn handle(&mut self, form: CommunityForm, _: &mut Context<Self>) -> 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
+        }
+        )
+      )
+  }
+}
index ca4fa368e2337526f138cf43efff30874d4e1414..08443b14b0d0871ae92bd7628d6f265494b2564f 100644 (file)
   },
   "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 (file)
index 0000000..dbacd18
--- /dev/null
@@ -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<any, State> {
+  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 (
+      <div class="container">
+        <div class="row">
+          <div class="col-12 col-lg-6 mb-4">
+            {this.communityForm()}
+          </div>
+        </div>
+      </div>
+    )
+  }
+
+  communityForm() {
+    return (
+      <div>
+        <form onSubmit={linkEvent(this, this.handleCreateCommunitySubmit)}>
+          <h3>Create Forum</h3>
+          <div class="form-group row">
+            <label class="col-sm-2 col-form-label">Name</label>
+            <div class="col-sm-10">
+              <input type="text" class="form-control" value={this.state.communityForm.name} onInput={linkEvent(this, this.handleCommunityNameChange)} required minLength={3} />
+            </div>
+          </div>
+          <div class="form-group row">
+            <div class="col-sm-10">
+              <button type="submit" class="btn btn-secondary">Create</button>
+            </div>
+          </div>
+        </form>
+      </div>
+    );
+  }
+  
+  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 (file)
index 0000000..bb6e60e
--- /dev/null
@@ -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<any, State> {
+
+  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 (
+      <div class="container">
+        <div class="row">
+          <div class="col-12 col-lg-6 mb-4">
+            create post
+            {/* {this.postForm()} */}
+          </div>
+        </div>
+      </div>
+    )
+  }
+
+  parseMessage(msg: any) {
+    console.log(msg);
+    let op: UserOperation = msgOp(msg);
+    if (msg.error) {
+      alert(msg.error);
+      return;
+    } else {
+    }
+  }
+
+}
index fd6f5045ca6e5740f840bfa5ef42bbbb9a9dc51e..372b15574d2ff9920ae585ebcdd36abd05505694 100644 (file)
@@ -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<any, State> {
+  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 (
       <div class="container">
@@ -51,7 +65,7 @@ export class Login extends Component<any, State> {
           <div class="form-group row">
             <label class="col-sm-2 col-form-label">Email or Username</label>
             <div class="col-sm-10">
-              <input type="text" class="form-control" value={this.state.loginForm.username} onInput={linkEvent(this, this.handleLoginUsernameChange)} required minLength={3} />
+              <input type="text" class="form-control" value={this.state.loginForm.username_or_email} onInput={linkEvent(this, this.handleLoginUsernameChange)} required minLength={3} />
             </div>
           </div>
           <div class="form-group row">
@@ -108,38 +122,55 @@ export class Login extends Component<any, State> {
   }
 
   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('/');
+      }
+    }
   }
 }
index 86d5d1d2ea93352fece865445cc729d0e64641b1..4cf6d6d27cecbcada3c4ff1773f4216457e0873b 100644 (file)
@@ -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<any, any> {
 
   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 (
-      <div class="sticky-top">{this.navbar()}</div>
+      <div>{this.navbar()}</div>
     )
   }
 
   // TODO class active corresponding to current page
+  // TODO toggle css collapse
   navbar() {
     return (
-      <nav class="navbar navbar-light bg-light p-0 px-3 shadow">
-        <a class="navbar-brand mx-1" href="#">
-          rrf
-        </a>
-        <ul class="navbar-nav mr-auto">
-          <li class="nav-item">
-            <a class="nav-item nav-link" href={repoUrl}>github</a>
-          </li>
-        </ul>
-        <ul class="navbar-nav ml-auto mr-2">
-          <li class="nav-item">
-            <Link class="nav-item nav-link" to="/login">Login</Link>
-          </li>
-        </ul>
+      <nav class="navbar navbar-expand-sm navbar-light bg-light p-0 px-3 shadow">
+        <a class="navbar-brand" href="#">rrf</a>
+        <button class="navbar-toggler" type="button" data-toggle="collapse" data-target="#navbarNav" aria-controls="navbarNav" aria-expanded="false" aria-label="Toggle navigation">
+          <span class="navbar-toggler-icon"></span>
+        </button>
+        <div class="collapse navbar-collapse">
+          <ul class="navbar-nav mr-auto">
+            <li class="nav-item">
+              <a class="nav-link" href={repoUrl}>github</a>
+            </li>
+            <li class="nav-item">
+              <Link class="nav-link" to="/create_post">Create Post</Link>
+            </li>
+            <li class="nav-item">
+              <Link class="nav-link" to="/create_community">Create Forum</Link>
+            </li>
+          </ul>
+          <ul class="navbar-nav ml-auto mr-2">
+            <li class="nav-item">
+              {this.state.isLoggedIn ? 
+              <a role="button" class="nav-link pointer" onClick={ linkEvent(this, this.handleLogoutClick) }>Logout</a> :
+              <Link class="nav-link" to="/login">Login</Link>
+              }
+            </li>
+          </ul>
+        </div>
       </nav>
     );
   }
 
+  handleLogoutClick(i: Navbar, event) {
+    UserService.Instance.logout();
+  }
 }
index 36e5681d65897c3da12ccdbecf79877b126c4518..1eb3a7d58f223f8b8d5d276b984f30891fa778bf 100644 (file)
@@ -4,10 +4,12 @@ import { HashRouter, Route, Switch } from 'inferno-router';
 import { Navbar } from './components/navbar';
 import { Home } from './components/home';
 import { Login } from './components/login';
+import { CreatePost } from './components/create-post';
+import { CreateCommunity } from './components/create-community';
 
 import './main.css';
 
-import { WebSocketService } from './services';
+import { WebSocketService, UserService } from './services';
 
 const container = document.getElementById('app');
 
@@ -16,6 +18,7 @@ class Index extends Component<any, any> {
   constructor(props, context) {
     super(props, context);
     WebSocketService.Instance;
+    UserService.Instance;
   }
 
   render() {
@@ -26,6 +29,8 @@ class Index extends Component<any, any> {
           <Switch>
             <Route exact path="/" component={Home} />
             <Route path={`/login`} component={Login} />
+            <Route path={`/create_post`} component={CreatePost} />
+            <Route path={`/create_community`} component={CreateCommunity} />
             {/*
             <Route path={`/search/:type_/:q/:page`} component={Search} />
             <Route path={`/submit`} component={Submit} />
index c1550cc158dae2a4c58b3e653092b33205dd4796..da5c415be40623b924e6263ed24242aad141811d 100644 (file)
@@ -1,7 +1,17 @@
-export interface LoginForm {
+export enum UserOperation {
+  Login, Register, CreateCommunity
+}
+
+export interface User {
+  id: number
   username: string;
+}
+
+export interface LoginForm {
+  username_or_email: string;
   password: string;
 }
+
 export interface RegisterForm {
   username: string;
   email?: string;
@@ -9,6 +19,14 @@ export interface RegisterForm {
   password_verify: string;
 }
 
-export enum UserOperation {
-  Login, Register
+export interface CommunityForm {
+  name: string;
+  updated?: number
+}
+
+export interface PostForm {
+  name: string;
+  url: string;
+  attributed_to: string;
+  updated?: number
 }
index e69de29bb2d1d6434b8b29ae775ad8c2e48c5391..77e093e8705627ce660e88edcaef88253af3d9f3 100644 (file)
@@ -0,0 +1,5 @@
+
+
+.pointer {
+  cursor: pointer;
+}
diff --git a/ui/src/services.ts b/ui/src/services.ts
deleted file mode 100644 (file)
index b9536ae..0000000
+++ /dev/null
@@ -1,57 +0,0 @@
-import { wsUri } from './env';
-import { LoginForm, RegisterForm, UserOperation } from './interfaces';
-
-export class WebSocketService {
-  private static _instance: WebSocketService;
-  private _ws;
-  private conn: WebSocket;
-
-  private constructor() {
-    console.log("Creating WSS");
-    this.connect();
-    console.log(wsUri);
-  }
-
-  public static get Instance(){
-    return this._instance || (this._instance = new this());
-  }
-
-  private connect() {
-    this.disconnect();
-    this.conn = new WebSocket(wsUri);
-    console.log('Connecting...');
-    this.conn.onopen = (() => {
-      console.log('Connected.');
-    });
-    this.conn.onmessage = (e => {
-      console.log('Received: ' + e.data);
-    });
-    this.conn.onclose = (() => {
-      console.log('Disconnected.');
-      this.conn = null;
-    });
-  }
-  private disconnect() {
-    if (this.conn != null) {
-      console.log('Disconnecting...');
-      this.conn.close();
-      this.conn = null;
-    }
-  }
-  
-  public login(loginForm: LoginForm) {
-    this.conn.send(this.wsSendWrapper(UserOperation.Login, loginForm));
-  }
-
-  public register(registerForm: RegisterForm) {
-    this.conn.send(this.wsSendWrapper(UserOperation.Register, registerForm));
-  }
-
-  private wsSendWrapper(op: UserOperation, data: any): string {
-    let send = { op: UserOperation[op], data: data };
-    console.log(send);
-    return JSON.stringify(send);
-  }
-
-
-}
diff --git a/ui/src/services/UserService.ts b/ui/src/services/UserService.ts
new file mode 100644 (file)
index 0000000..af0d1d1
--- /dev/null
@@ -0,0 +1,51 @@
+import * as Cookies from 'js-cookie';
+import { User } from '../interfaces';
+import * as jwt_decode from 'jwt-decode';
+import { Subject } from 'rxjs';
+
+export class UserService {
+  private static _instance: UserService;
+  private user: User;
+  public sub: Subject<User> = new Subject<User>();
+
+  private constructor() {
+    let jwt = Cookies.get("jwt");
+    if (jwt) {
+      this.setUser(jwt);
+    } else {
+      console.log('No JWT cookie found.');
+    }
+
+  }
+
+  public login(jwt: string) {
+    Cookies.set("jwt", jwt);
+    console.log("jwt cookie set");
+    this.setUser(jwt);
+  }
+
+  public logout() {
+    this.user = null;
+    Cookies.remove("jwt");
+    console.log("Logged out.");
+    this.sub.next(null);
+  }
+
+  public get loggedIn(): boolean {
+    return this.user !== undefined;
+  }
+
+  public get auth(): string {
+    return Cookies.get("jwt");
+  }
+
+  private setUser(jwt: string) {
+    this.user = jwt_decode(jwt);
+    this.sub.next(this.user);
+    console.log(this.user.username);
+  }
+
+  public static get Instance(){
+    return this._instance || (this._instance = new this());
+  }
+}
diff --git a/ui/src/services/WebSocketService.ts b/ui/src/services/WebSocketService.ts
new file mode 100644 (file)
index 0000000..1882b12
--- /dev/null
@@ -0,0 +1,37 @@
+import { wsUri } from '../env';
+import { LoginForm, RegisterForm, UserOperation, CommunityForm } from '../interfaces';
+import { webSocket } from 'rxjs/webSocket';
+import { Subject } from 'rxjs';
+import { UserService } from './';
+
+export class WebSocketService {
+  private static _instance: WebSocketService;
+  public subject: Subject<{}>;
+
+  private constructor() {
+    this.subject = webSocket(wsUri);
+    console.log(`Connected to ${wsUri}`);
+  }
+
+  public static get Instance(){
+    return this._instance || (this._instance = new this());
+  }
+   
+  public login(loginForm: LoginForm) {
+    this.subject.next(this.wsSendWrapper(UserOperation.Login, loginForm));
+  }
+
+  public register(registerForm: RegisterForm) {
+    this.subject.next(this.wsSendWrapper(UserOperation.Register, registerForm));
+  }
+
+  public createCommunity(communityForm: CommunityForm) {
+    this.subject.next(this.wsSendWrapper(UserOperation.CreateCommunity, communityForm, UserService.Instance.auth));
+  }
+
+  private wsSendWrapper(op: UserOperation, data: any, auth?: string) {
+    let send = { op: UserOperation[op], data: data, auth: auth };
+    console.log(send);
+    return send;
+  }
+}
diff --git a/ui/src/services/index.ts b/ui/src/services/index.ts
new file mode 100644 (file)
index 0000000..f0f4ccf
--- /dev/null
@@ -0,0 +1,2 @@
+export { UserService } from './UserService';
+export { WebSocketService } from './WebSocketService';
index e141c68102ac08f1d46df87ee265af3f65cd0843..d3d9696eb36f46f75b6a0600f4e7157e9fa0c012 100644 (file)
@@ -1,2 +1,9 @@
+import { UserOperation } from './interfaces';
+
 export let repoUrl = 'https://github.com/dessalines/rust-reddit-fediverse';
 export let wsUri = (window.location.protocol=='https:'&&'wss://'||'ws://')+window.location.host + '/service/ws/';
+
+export function msgOp(msg: any): UserOperation {
+  let opStr: string = msg.op;
+  return UserOperation[opStr];
+}
index 92a1250a2e78095fa313a790b4724975b0f1271a..e668e749156c2dd18b194c7cc2b0038872350a6b 100644 (file)
@@ -9,6 +9,11 @@
   dependencies:
     regenerator-runtime "^0.12.0"
 
+"@types/js-cookie@^2.2.1":
+  version "2.2.1"
+  resolved "https://registry.yarnpkg.com/@types/js-cookie/-/js-cookie-2.2.1.tgz#aa6f6d5e5aaf7d97959e9fa938ac2501cf1a76f4"
+  integrity sha512-VIVurImEhQ95jxtjs8baVU5qCzVfwYfuMrpXwdRykJ5MCI5iY7/jB4cDSgwBVeYqeXrhT7GfJUwoDOmN0OMVCA==
+
 abbrev@1:
   version "1.1.1"
   resolved "https://registry.yarnpkg.com/abbrev/-/abbrev-1.1.1.tgz#f8f2c887ad10bf67f634f005b6987fed3179aac8"
@@ -1456,6 +1461,11 @@ isstream@~0.1.2:
   resolved "https://registry.yarnpkg.com/isstream/-/isstream-0.1.2.tgz#47e63f7af55afa6f92e1500e690eb8b8529c099a"
   integrity sha1-R+Y/evVa+m+S4VAOaQ64uFKcCZo=
 
+js-cookie@^2.2.0:
+  version "2.2.0"
+  resolved "https://registry.yarnpkg.com/js-cookie/-/js-cookie-2.2.0.tgz#1b2c279a6eece380a12168b92485265b35b1effb"
+  integrity sha1-Gywnmm7s44ChIWi5JIUmWzWx7/s=
+
 "js-tokens@^3.0.0 || ^4.0.0":
   version "4.0.0"
   resolved "https://registry.yarnpkg.com/js-tokens/-/js-tokens-4.0.0.tgz#19203fb59991df98e3a287050d4647cdeaf32499"
@@ -1503,6 +1513,11 @@ jsprim@^1.2.2:
     json-schema "0.2.3"
     verror "1.10.0"
 
+jwt-decode@^2.2.0:
+  version "2.2.0"
+  resolved "https://registry.yarnpkg.com/jwt-decode/-/jwt-decode-2.2.0.tgz#7d86bd56679f58ce6a84704a657dd392bba81a79"
+  integrity sha1-fYa9VmefWM5qhHBKZX3TkruoGnk=
+
 kind-of@^3.0.2, kind-of@^3.0.3, kind-of@^3.2.0:
   version "3.2.2"
   resolved "https://registry.yarnpkg.com/kind-of/-/kind-of-3.2.2.tgz#31ea21a734bab9bbb0f32466d893aea51e4a3c64"
@@ -2446,6 +2461,13 @@ rx-lite@*, rx-lite@^4.0.8:
   resolved "https://registry.yarnpkg.com/rx-lite/-/rx-lite-4.0.8.tgz#0b1e11af8bc44836f04a6407e92da42467b79444"
   integrity sha1-Cx4Rr4vESDbwSmQH6S2kJGe3lEQ=
 
+rxjs@^6.4.0:
+  version "6.4.0"
+  resolved "https://registry.yarnpkg.com/rxjs/-/rxjs-6.4.0.tgz#f3bb0fe7bda7fb69deac0c16f17b50b0b8790504"
+  integrity sha512-Z9Yfa11F6B9Sg/BK9MnqnQ+aQYicPLtilXBp2yUtDt2JRCE0h26d33EnfO3ZxoNxG0T92OUucP3Ct7cpfkdFfw==
+  dependencies:
+    tslib "^1.9.0"
+
 safe-buffer@5.1.2, safe-buffer@^5.0.1, safe-buffer@^5.1.2, safe-buffer@~5.1.0, safe-buffer@~5.1.1:
   version "5.1.2"
   resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.1.2.tgz#991ec69d296e0313747d59bdfd2b745c35f8828d"
@@ -2824,7 +2846,7 @@ ts-transform-inferno@^4.0.2:
   resolved "https://registry.yarnpkg.com/ts-transform-inferno/-/ts-transform-inferno-4.0.2.tgz#06b9be45edf874ba7a6ebfb6107ba782509c6afe"
   integrity sha512-CZb4+w/2l2zikPZ/c51fi3n+qnR2HCEfAS73oGQB80aqRLffkZqm25kYYTMmqUW2+oVfs4M5AZa0z14cvxlQ5w==
 
-tslib@^1.8.0:
+tslib@^1.8.0, tslib@^1.9.0:
   version "1.9.3"
   resolved "https://registry.yarnpkg.com/tslib/-/tslib-1.9.3.tgz#d7e4dd79245d85428c4d7e4822a79917954ca286"
   integrity sha512-4krF8scpejhaOgqzBEcGM7yDIEfi0/8+8zDRZhNZZ2kjmHJ4hv3zCbQWxoJGz1iw5U0Jl0nma13xzHXcncMavQ==