- Login and Register mostly working.
- Starting to work on creating communities.
- [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
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"
"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)",
"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"
rand = "0.6.5"
strum = "0.14.0"
strum_macros = "0.14.0"
+jsonwebtoken = "*"
+regex = "1"
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
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
);
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)]
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.")
}
}
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,
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,
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);
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,
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)]
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)]
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.")
}
}
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,
};
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,
};
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,
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);
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)]
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.")
}
}
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,
};
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,
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);
-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_"]
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)
}
}
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);
}
}
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 {
/// 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();
// 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 {
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();
.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)),
}
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;
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 {
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 {
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"));
+ }
+}
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)]
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,
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 {
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()
+ }
+ )
+ )
}
}
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
+ }
+ )
+ )
+ }
+}
},
"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",
--- /dev/null
+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 {
+ }
+ }
+
+}
--- /dev/null
+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 {
+ }
+ }
+
+}
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;
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">
<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">
}
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('/');
+ }
+ }
}
}
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();
+ }
}
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');
constructor(props, context) {
super(props, context);
WebSocketService.Instance;
+ UserService.Instance;
}
render() {
<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} />
-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;
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
}
+
+
+.pointer {
+ cursor: pointer;
+}
+++ /dev/null
-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);
- }
-
-
-}
--- /dev/null
+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());
+ }
+}
--- /dev/null
+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;
+ }
+}
--- /dev/null
+export { UserService } from './UserService';
+export { WebSocketService } from './WebSocketService';
+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];
+}
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"
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"
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"
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"
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==