Adding login and Register

- Login and Register  mostly working.
- Starting to work on creating communities.
This commit is contained in:
Dessalines 2019-03-22 18:42:57 -07:00
parent d52c16c123
commit c438f0fef1
26 changed files with 827 additions and 302 deletions

View File

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

17
server/Cargo.lock generated
View 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"

View File

@ -18,3 +18,5 @@ env_logger = "*"
rand = "0.6.5"
strum = "0.14.0"
strum_macros = "0.14.0"
jsonwebtoken = "*"
regex = "1"

View 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

View 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
);

View 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);

View 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);

View 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);

View 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;
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::<User_>(conn)
.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);
}
}

View 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();
@ -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)),
}

View 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 {
@ -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;
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"));
}
}

View 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
}
pub enum MessageType {
Comments, Users, Ping, Pong
Login, Register, Logout, CreateCommunity, Join, Edit, Reply, Vote, Delete, NextPage, Sticky
}
#[derive(EnumString,ToString,Debug)]
pub enum MessageToUser {
Comments, Users, Ping, Pong, Error
}
#[derive(Serialize, Deserialize)]
pub struct ErrorMessage {
op: String,
error: String
}
/// Chat server sends this messages to session
#[derive(Message)]
@ -66,14 +74,16 @@ pub struct Join {
pub name: String,
}
#[derive(Message)]
#[derive(Serialize, Deserialize)]
pub struct Login {
pub username: String,
pub username_or_email: String,
pub password: String
}
// #[derive(Message)]
impl actix::Message for Login {
type Result = Result<LoginResponse, ErrorMessage>;
}
#[derive(Serialize, Deserialize)]
pub struct Register {
username: String,
@ -82,9 +92,31 @@ pub struct Register {
password_verify: String
}
impl actix::Message for Register {
type Result = String;
#[derive(Serialize, Deserialize)]
pub struct LoginResponse {
op: String,
jwt: String
}
impl actix::Message for Register {
type Result = Result<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
}
)
)
}
}

View File

@ -15,11 +15,15 @@
},
"engineStrict": true,
"dependencies": {
"@types/js-cookie": "^2.2.1",
"classcat": "^1.1.3",
"dotenv": "^6.1.0",
"inferno": "^7.0.1",
"inferno-router": "^7.0.1",
"moment": "^2.22.2"
"js-cookie": "^2.2.0",
"jwt-decode": "^2.2.0",
"moment": "^2.22.2",
"rxjs": "^6.4.0"
},
"devDependencies": {
"fuse-box": "3.1.3",

View File

@ -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 {
}
}
}

View File

@ -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 {
}
}
}

View 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('/');
}
}
}
}

View 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>
<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-item nav-link" href={repoUrl}>github</a>
<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">
<Link class="nav-item nav-link" to="/login">Login</Link>
{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();
}
}

View 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} />

View 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
}

View File

@ -0,0 +1,5 @@
.pointer {
cursor: pointer;
}

View File

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

View File

@ -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());
}
}

View File

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

2
ui/src/services/index.ts Normal file
View File

@ -0,0 +1,2 @@
export { UserService } from './UserService';
export { WebSocketService } from './WebSocketService';

View 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];
}

View 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==