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) - [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 - 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) - [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 ## TODOs
- Endpoints - Endpoints

17
server/Cargo.lock generated
View File

@ -703,6 +703,20 @@ name = "itoa"
version = "0.4.3" version = "0.4.3"
source = "registry+https://github.com/rust-lang/crates.io-index" 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]] [[package]]
name = "kernel32-sys" name = "kernel32-sys"
version = "0.2.2" version = "0.2.2"
@ -1309,7 +1323,9 @@ dependencies = [
"dotenv 0.9.0 (registry+https://github.com/rust-lang/crates.io-index)", "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)", "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)", "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)", "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 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)", "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)", "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 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 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 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 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 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" "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" rand = "0.6.5"
strum = "0.14.0" strum = "0.14.0"
strum_macros = "0.14.0" strum_macros = "0.14.0"
jsonwebtoken = "*"
regex = "1"

View File

@ -1,9 +1,9 @@
create table user_ ( create table user_ (
id serial primary key, id serial primary key,
name varchar(20) not null, name varchar(20) not null unique,
preferred_username varchar(20), preferred_username varchar(20),
password_encrypted text not null, password_encrypted text not null,
email text, email text unique,
icon bytea, icon bytea,
published timestamp not null default now(), published timestamp not null default now(),
updated timestamp updated timestamp

View File

@ -1,6 +1,6 @@
create table community ( create table community (
id serial primary key, id serial primary key,
name varchar(20) not null, name varchar(20) not null unique,
published timestamp not null default now(), published timestamp not null default now(),
updated timestamp updated timestamp
); );

View File

@ -23,14 +23,14 @@ pub struct Comment {
pub updated: Option<chrono::NaiveDateTime> pub updated: Option<chrono::NaiveDateTime>
} }
#[derive(Insertable, AsChangeset, Clone, Copy)] #[derive(Insertable, AsChangeset, Clone)]
#[table_name="comment"] #[table_name="comment"]
pub struct CommentForm<'a> { pub struct CommentForm {
pub content: &'a str, pub content: String,
pub attributed_to: &'a str, pub attributed_to: String,
pub post_id: &'a i32, pub post_id: i32,
pub parent_id: Option<&'a i32>, pub parent_id: Option<i32>,
pub updated: Option<&'a chrono::NaiveDateTime> pub updated: Option<chrono::NaiveDateTime>
} }
#[derive(Identifiable, Queryable, Associations, PartialEq, Debug)] #[derive(Identifiable, Queryable, Associations, PartialEq, Debug)]
@ -44,59 +44,55 @@ pub struct CommentLike {
pub published: chrono::NaiveDateTime, pub published: chrono::NaiveDateTime,
} }
#[derive(Insertable, AsChangeset, Clone, Copy)] #[derive(Insertable, AsChangeset, Clone)]
#[table_name="comment_like"] #[table_name="comment_like"]
pub struct CommentLikeForm<'a> { pub struct CommentLikeForm {
pub comment_id: &'a i32, pub comment_id: i32,
pub fedi_user_id: &'a str, pub fedi_user_id: String,
pub score: &'a i16 pub score: i16
} }
impl<'a> Crud<CommentForm<'a>> for Comment { impl Crud<CommentForm> for Comment {
fn read(conn: &PgConnection, comment_id: i32) -> Comment { fn read(conn: &PgConnection, comment_id: i32) -> Result<Self, Error> {
use schema::comment::dsl::*; use schema::comment::dsl::*;
comment.find(comment_id) comment.find(comment_id)
.first::<Comment>(conn) .first::<Self>(conn)
.expect("Error in query")
} }
fn delete(conn: &PgConnection, comment_id: i32) -> usize { fn delete(conn: &PgConnection, comment_id: i32) -> Result<usize, Error> {
use schema::comment::dsl::*; use schema::comment::dsl::*;
diesel::delete(comment.find(comment_id)) diesel::delete(comment.find(comment_id))
.execute(conn) .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::*; use schema::comment::dsl::*;
insert_into(comment) insert_into(comment)
.values(comment_form) .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::*; use schema::comment::dsl::*;
diesel::update(comment.find(comment_id)) diesel::update(comment.find(comment_id))
.set(comment_form) .set(comment_form)
.get_result::<Comment>(conn) .get_result::<Self>(conn)
.expect(&format!("Unable to find {}", comment_id))
} }
} }
impl<'a> Likeable <CommentLikeForm<'a>> for CommentLike { impl Likeable <CommentLikeForm> for CommentLike {
fn like(conn: &PgConnection, comment_like_form: CommentLikeForm) -> Result<CommentLike, Error> { fn like(conn: &PgConnection, comment_like_form: &CommentLikeForm) -> Result<Self, Error> {
use schema::comment_like::dsl::*; use schema::comment_like::dsl::*;
insert_into(comment_like) insert_into(comment_like)
.values(comment_like_form) .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::*; use schema::comment_like::dsl::*;
diesel::delete(comment_like diesel::delete(comment_like
.filter(comment_id.eq(comment_like_form.comment_id)) .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) .execute(conn)
.expect("Error deleting.")
} }
} }
@ -117,17 +113,17 @@ mod tests {
updated: None updated: None
}; };
let inserted_post = Post::create(&conn, new_post).unwrap(); let inserted_post = Post::create(&conn, &new_post).unwrap();
let comment_form = CommentForm { let comment_form = CommentForm {
content: "A test comment".into(), content: "A test comment".into(),
attributed_to: "test_user.com".into(), attributed_to: "test_user.com".into(),
post_id: &inserted_post.id, post_id: inserted_post.id,
parent_id: None, parent_id: None,
updated: 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 { let expected_comment = Comment {
id: inserted_comment.id, id: inserted_comment.id,
@ -142,20 +138,20 @@ mod tests {
let child_comment_form = CommentForm { let child_comment_form = CommentForm {
content: "A child comment".into(), content: "A child comment".into(),
attributed_to: "test_user.com".into(), attributed_to: "test_user.com".into(),
post_id: &inserted_post.id, post_id: inserted_post.id,
parent_id: Some(&inserted_comment.id), parent_id: Some(inserted_comment.id),
updated: None 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 { let comment_like_form = CommentLikeForm {
comment_id: &inserted_comment.id, comment_id: inserted_comment.id,
fedi_user_id: "test".into(), 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 { let expected_comment_like = CommentLike {
id: inserted_comment_like.id, id: inserted_comment_like.id,
@ -165,12 +161,12 @@ mod tests {
score: 1 score: 1
}; };
let read_comment = Comment::read(&conn, inserted_comment.id); let read_comment = Comment::read(&conn, inserted_comment.id).unwrap();
let updated_comment = Comment::update(&conn, inserted_comment.id, comment_form); let updated_comment = Comment::update(&conn, inserted_comment.id, &comment_form).unwrap();
let like_removed = CommentLike::remove(&conn, comment_like_form); let like_removed = CommentLike::remove(&conn, &comment_like_form).unwrap();
let num_deleted = Comment::delete(&conn, inserted_comment.id); let num_deleted = Comment::delete(&conn, inserted_comment.id).unwrap();
Comment::delete(&conn, inserted_child_comment.id); Comment::delete(&conn, inserted_child_comment.id).unwrap();
Post::delete(&conn, inserted_post.id); Post::delete(&conn, inserted_post.id).unwrap();
assert_eq!(expected_comment, read_comment); assert_eq!(expected_comment, read_comment);
assert_eq!(expected_comment, inserted_comment); assert_eq!(expected_comment, inserted_comment);

View File

@ -2,9 +2,10 @@ extern crate diesel;
use schema::{community, community_user, community_follower}; use schema::{community, community_user, community_follower};
use diesel::*; use diesel::*;
use diesel::result::Error; use diesel::result::Error;
use serde::{Deserialize, Serialize};
use {Crud, Followable, Joinable}; use {Crud, Followable, Joinable};
#[derive(Queryable, Identifiable, PartialEq, Debug)] #[derive(Queryable, Identifiable, PartialEq, Debug, Serialize, Deserialize)]
#[table_name="community"] #[table_name="community"]
pub struct Community { pub struct Community {
pub id: i32, pub id: i32,
@ -13,11 +14,11 @@ pub struct Community {
pub updated: Option<chrono::NaiveDateTime> pub updated: Option<chrono::NaiveDateTime>
} }
#[derive(Insertable, AsChangeset, Clone, Copy)] #[derive(Insertable, AsChangeset, Clone, Serialize, Deserialize)]
#[table_name="community"] #[table_name="community"]
pub struct CommunityForm<'a> { pub struct CommunityForm {
pub name: &'a str, pub name: String,
pub updated: Option<&'a chrono::NaiveDateTime> pub updated: Option<chrono::NaiveDateTime>
} }
#[derive(Identifiable, Queryable, Associations, PartialEq, Debug)] #[derive(Identifiable, Queryable, Associations, PartialEq, Debug)]
@ -30,11 +31,11 @@ pub struct CommunityUser {
pub published: chrono::NaiveDateTime, pub published: chrono::NaiveDateTime,
} }
#[derive(Insertable, AsChangeset, Clone, Copy)] #[derive(Insertable, AsChangeset, Clone)]
#[table_name="community_user"] #[table_name="community_user"]
pub struct CommunityUserForm<'a> { pub struct CommunityUserForm {
pub community_id: &'a i32, pub community_id: i32,
pub fedi_user_id: &'a str, pub fedi_user_id: String,
} }
#[derive(Identifiable, Queryable, Associations, PartialEq, Debug)] #[derive(Identifiable, Queryable, Associations, PartialEq, Debug)]
@ -47,76 +48,72 @@ pub struct CommunityFollower {
pub published: chrono::NaiveDateTime, pub published: chrono::NaiveDateTime,
} }
#[derive(Insertable, AsChangeset, Clone, Copy)] #[derive(Insertable, AsChangeset, Clone)]
#[table_name="community_follower"] #[table_name="community_follower"]
pub struct CommunityFollowerForm<'a> { pub struct CommunityFollowerForm {
pub community_id: &'a i32, pub community_id: i32,
pub fedi_user_id: &'a str, pub fedi_user_id: String,
} }
impl<'a> Crud<CommunityForm<'a>> for Community { impl Crud<CommunityForm> for Community {
fn read(conn: &PgConnection, community_id: i32) -> Community { fn read(conn: &PgConnection, community_id: i32) -> Result<Self, Error> {
use schema::community::dsl::*; use schema::community::dsl::*;
community.find(community_id) community.find(community_id)
.first::<Community>(conn) .first::<Self>(conn)
.expect("Error in query")
} }
fn delete(conn: &PgConnection, community_id: i32) -> usize { fn delete(conn: &PgConnection, community_id: i32) -> Result<usize, Error> {
use schema::community::dsl::*; use schema::community::dsl::*;
diesel::delete(community.find(community_id)) diesel::delete(community.find(community_id))
.execute(conn) .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::*; use schema::community::dsl::*;
insert_into(community) insert_into(community)
.values(new_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::*; use schema::community::dsl::*;
diesel::update(community.find(community_id)) diesel::update(community.find(community_id))
.set(new_community) .set(new_community)
.get_result::<Community>(conn) .get_result::<Self>(conn)
.expect(&format!("Unable to find {}", community_id))
} }
} }
impl<'a> Followable<CommunityFollowerForm<'a>> for CommunityFollower { impl Followable<CommunityFollowerForm> for CommunityFollower {
fn follow(conn: &PgConnection, community_follower_form: CommunityFollowerForm) -> Result<CommunityFollower, Error> { fn follow(conn: &PgConnection, community_follower_form: &CommunityFollowerForm) -> Result<Self, Error> {
use schema::community_follower::dsl::*; use schema::community_follower::dsl::*;
insert_into(community_follower) insert_into(community_follower)
.values(community_follower_form) .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::*; use schema::community_follower::dsl::*;
diesel::delete(community_follower diesel::delete(community_follower
.filter(community_id.eq(community_follower_form.community_id)) .filter(community_id.eq(&community_follower_form.community_id))
.filter(fedi_user_id.eq(community_follower_form.fedi_user_id))) .filter(fedi_user_id.eq(&community_follower_form.fedi_user_id)))
.execute(conn) .execute(conn)
.expect("Error deleting.")
} }
} }
impl<'a> Joinable<CommunityUserForm<'a>> for CommunityUser { impl Joinable<CommunityUserForm> for CommunityUser {
fn join(conn: &PgConnection, community_user_form: CommunityUserForm) -> Result<CommunityUser, Error> { fn join(conn: &PgConnection, community_user_form: &CommunityUserForm) -> Result<Self, Error> {
use schema::community_user::dsl::*; use schema::community_user::dsl::*;
insert_into(community_user) insert_into(community_user)
.values(community_user_form) .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::*; use schema::community_user::dsl::*;
diesel::delete(community_user diesel::delete(community_user
.filter(community_id.eq(community_user_form.community_id)) .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) .execute(conn)
.expect("Error deleting.")
} }
} }
@ -135,7 +132,7 @@ mod tests {
updated: None updated: None
}; };
let inserted_community = Community::create(&conn, new_community).unwrap(); let inserted_community = Community::create(&conn, &new_community).unwrap();
let expected_community = Community { let expected_community = Community {
id: inserted_community.id, id: inserted_community.id,
@ -145,21 +142,21 @@ mod tests {
}; };
let new_user = UserForm { let new_user = UserForm {
name: "thom".into(), name: "terry".into(),
preferred_username: None, preferred_username: None,
password_encrypted: "nope".into(), password_encrypted: "nope".into(),
email: None, email: None,
updated: 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 { let community_follower_form = CommunityFollowerForm {
community_id: &inserted_community.id, community_id: inserted_community.id,
fedi_user_id: "test".into() 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 { let expected_community_follower = CommunityFollower {
id: inserted_community_follower.id, id: inserted_community_follower.id,
@ -169,11 +166,11 @@ mod tests {
}; };
let community_user_form = CommunityUserForm { let community_user_form = CommunityUserForm {
community_id: &inserted_community.id, community_id: inserted_community.id,
fedi_user_id: "test".into() 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 { let expected_community_user = CommunityUser {
id: inserted_community_user.id, id: inserted_community_user.id,
@ -182,12 +179,12 @@ mod tests {
published: inserted_community_user.published published: inserted_community_user.published
}; };
let read_community = Community::read(&conn, inserted_community.id); let read_community = Community::read(&conn, inserted_community.id).unwrap();
let updated_community = Community::update(&conn, inserted_community.id, new_community); let updated_community = Community::update(&conn, inserted_community.id, &new_community).unwrap();
let ignored_community = CommunityFollower::ignore(&conn, community_follower_form); let ignored_community = CommunityFollower::ignore(&conn, &community_follower_form).unwrap();
let left_community = CommunityUser::leave(&conn, community_user_form); let left_community = CommunityUser::leave(&conn, &community_user_form).unwrap();
let num_deleted = Community::delete(&conn, inserted_community.id); let num_deleted = Community::delete(&conn, inserted_community.id).unwrap();
User_::delete(&conn, inserted_user.id); User_::delete(&conn, inserted_user.id).unwrap();
assert_eq!(expected_community, read_community); assert_eq!(expected_community, read_community);
assert_eq!(expected_community, inserted_community); assert_eq!(expected_community, inserted_community);

View File

@ -15,13 +15,13 @@ pub struct Post {
pub updated: Option<chrono::NaiveDateTime> pub updated: Option<chrono::NaiveDateTime>
} }
#[derive(Insertable, AsChangeset, Clone, Copy)] #[derive(Insertable, AsChangeset, Clone)]
#[table_name="post"] #[table_name="post"]
pub struct PostForm<'a> { pub struct PostForm {
pub name: &'a str, pub name: String,
pub url: &'a str, pub url: String,
pub attributed_to: &'a str, pub attributed_to: String,
pub updated: Option<&'a chrono::NaiveDateTime> pub updated: Option<chrono::NaiveDateTime>
} }
#[derive(Identifiable, Queryable, Associations, PartialEq, Debug)] #[derive(Identifiable, Queryable, Associations, PartialEq, Debug)]
@ -35,59 +35,55 @@ pub struct PostLike {
pub published: chrono::NaiveDateTime, pub published: chrono::NaiveDateTime,
} }
#[derive(Insertable, AsChangeset, Clone, Copy)] #[derive(Insertable, AsChangeset, Clone)]
#[table_name="post_like"] #[table_name="post_like"]
pub struct PostLikeForm<'a> { pub struct PostLikeForm {
pub post_id: &'a i32, pub post_id: i32,
pub fedi_user_id: &'a str, pub fedi_user_id: String,
pub score: &'a i16 pub score: i16
} }
impl<'a> Crud<PostForm<'a>> for Post { impl Crud<PostForm> for Post {
fn read(conn: &PgConnection, post_id: i32) -> Post { fn read(conn: &PgConnection, post_id: i32) -> Result<Self, Error> {
use schema::post::dsl::*; use schema::post::dsl::*;
post.find(post_id) post.find(post_id)
.first::<Post>(conn) .first::<Self>(conn)
.expect("Error in query")
} }
fn delete(conn: &PgConnection, post_id: i32) -> usize { fn delete(conn: &PgConnection, post_id: i32) -> Result<usize, Error> {
use schema::post::dsl::*; use schema::post::dsl::*;
diesel::delete(post.find(post_id)) diesel::delete(post.find(post_id))
.execute(conn) .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::*; use schema::post::dsl::*;
insert_into(post) insert_into(post)
.values(new_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::*; use schema::post::dsl::*;
diesel::update(post.find(post_id)) diesel::update(post.find(post_id))
.set(new_post) .set(new_post)
.get_result::<Post>(conn) .get_result::<Self>(conn)
.expect(&format!("Unable to find {}", post_id))
} }
} }
impl<'a> Likeable <PostLikeForm<'a>> for PostLike { impl Likeable <PostLikeForm> for PostLike {
fn like(conn: &PgConnection, post_like_form: PostLikeForm) -> Result<PostLike, Error> { fn like(conn: &PgConnection, post_like_form: &PostLikeForm) -> Result<Self, Error> {
use schema::post_like::dsl::*; use schema::post_like::dsl::*;
insert_into(post_like) insert_into(post_like)
.values(post_like_form) .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::*; use schema::post_like::dsl::*;
diesel::delete(post_like diesel::delete(post_like
.filter(post_id.eq(post_like_form.post_id)) .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) .execute(conn)
.expect("Error deleting.")
} }
} }
@ -107,7 +103,7 @@ mod tests {
updated: None updated: None
}; };
let inserted_post = Post::create(&conn, new_post).unwrap(); let inserted_post = Post::create(&conn, &new_post).unwrap();
let expected_post = Post { let expected_post = Post {
id: inserted_post.id, id: inserted_post.id,
@ -119,12 +115,12 @@ mod tests {
}; };
let post_like_form = PostLikeForm { let post_like_form = PostLikeForm {
post_id: &inserted_post.id, post_id: inserted_post.id,
fedi_user_id: "test".into(), 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 { let expected_post_like = PostLike {
id: inserted_post_like.id, id: inserted_post_like.id,
@ -134,10 +130,10 @@ mod tests {
score: 1 score: 1
}; };
let read_post = Post::read(&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); let updated_post = Post::update(&conn, inserted_post.id, &new_post).unwrap();
let like_removed = PostLike::remove(&conn, post_like_form); let like_removed = PostLike::remove(&conn, &post_like_form).unwrap();
let num_deleted = Post::delete(&conn, inserted_post.id); let num_deleted = Post::delete(&conn, inserted_post.id).unwrap();
assert_eq!(expected_post, read_post); assert_eq!(expected_post, read_post);
assert_eq!(expected_post, inserted_post); assert_eq!(expected_post, inserted_post);

View File

@ -1,9 +1,11 @@
extern crate diesel;
use schema::user_; use schema::user_;
use diesel::*; use diesel::*;
use diesel::result::Error; use diesel::result::Error;
use schema::user_::dsl::*; 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)] #[derive(Queryable, Identifiable, PartialEq, Debug)]
#[table_name="user_"] #[table_name="user_"]
@ -18,43 +20,75 @@ pub struct User_ {
pub updated: Option<chrono::NaiveDateTime> pub updated: Option<chrono::NaiveDateTime>
} }
#[derive(Insertable, AsChangeset, Clone, Copy)] #[derive(Insertable, AsChangeset, Clone)]
#[table_name="user_"] #[table_name="user_"]
pub struct UserForm<'a> { pub struct UserForm {
pub name: &'a str, pub name: String,
pub preferred_username: Option<&'a str>, pub preferred_username: Option<String>,
pub password_encrypted: &'a str, pub password_encrypted: String,
pub email: Option<&'a str>, pub email: Option<String>,
pub updated: Option<&'a chrono::NaiveDateTime> pub updated: Option<chrono::NaiveDateTime>
} }
impl<'a> Crud<UserForm<'a>> for User_ { impl Crud<UserForm> for User_ {
fn read(conn: &PgConnection, user_id: i32) -> User_ { fn read(conn: &PgConnection, user_id: i32) -> Result<Self, Error> {
user_.find(user_id) user_.find(user_id)
.first::<User_>(conn) .first::<Self>(conn)
.expect("Error in query")
} }
fn delete(conn: &PgConnection, user_id: i32) -> usize { fn delete(conn: &PgConnection, user_id: i32) -> Result<usize, Error> {
diesel::delete(user_.find(user_id)) diesel::delete(user_.find(user_id))
.execute(conn) .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(); let mut edited_user = form.clone();
// Add the rust crypt let password_hash = hash(&form.password_encrypted, DEFAULT_COST)
edited_user.password_encrypted = "here"; .expect("Couldn't hash password");
// edited_user.password_encrypted; edited_user.password_encrypted = password_hash;
insert_into(user_) insert_into(user_)
.values(edited_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(); 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)) diesel::update(user_.find(user_id))
.set(edited_user) .set(edited_user)
.get_result::<User_>(conn) .get_result::<Self>(conn)
.expect(&format!("Unable to find user {}", user_id)) }
}
#[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 updated: None
}; };
let inserted_user = User_::create(&conn, new_user).unwrap(); let inserted_user = User_::create(&conn, &new_user).unwrap();
let expected_user = User_ { let expected_user = User_ {
id: inserted_user.id, id: inserted_user.id,
name: "thom".into(), name: "thom".into(),
preferred_username: None, preferred_username: None,
password_encrypted: "here".into(), password_encrypted: "$2y$12$YXpNpYsdfjmed.QlYLvw4OfTCgyKUnKHc/V8Dgcf9YcVKHPaYXYYy".into(),
email: None, email: None,
icon: None, icon: None,
published: inserted_user.published, published: inserted_user.published,
updated: None updated: None
}; };
let read_user = User_::read(&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); let updated_user = User_::update(&conn, inserted_user.id, &new_user).unwrap();
let num_deleted = User_::delete(&conn, inserted_user.id); let num_deleted = User_::delete(&conn, inserted_user.id).unwrap();
assert_eq!(expected_user, read_user); assert_eq!(expected_user.id, read_user.id);
assert_eq!(expected_user, inserted_user); assert_eq!(expected_user.id, inserted_user.id);
assert_eq!(expected_user, updated_user); assert_eq!(expected_user.id, updated_user.id);
assert_eq!(1, num_deleted); assert_eq!(1, num_deleted);
} }
} }

View File

@ -4,17 +4,15 @@ use std::time::{Instant, Duration};
use server::actix::*; use server::actix::*;
use server::actix_web::server::HttpServer; use server::actix_web::server::HttpServer;
use server::actix_web::{fs, http, ws, App, Error, HttpRequest, HttpResponse}; 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 /// How often heartbeat pings are sent
const HEARTBEAT_INTERVAL: Duration = Duration::from_secs(5); const HEARTBEAT_INTERVAL: Duration = Duration::from_secs(5);
/// How long before lack of client response causes a timeout /// How long before lack of client response causes a timeout
const CLIENT_TIMEOUT: Duration = Duration::from_secs(10); 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 /// This is our websocket route state, this state is shared with all route
/// instances via `HttpContext::state()` /// instances via `HttpContext::state()`
struct WsChatSessionState { struct WsChatSessionState {
@ -92,7 +90,7 @@ use server::serde_json::Value;
/// WebSocket message handler /// WebSocket message handler
impl StreamHandler<ws::Message, ws::ProtocolError> for WSSession { impl StreamHandler<ws::Message, ws::ProtocolError> for WSSession {
fn handle(&mut self, msg: ws::Message, ctx: &mut Self::Context) { fn handle(&mut self, msg: ws::Message, ctx: &mut Self::Context) {
// println!("WEBSOCKET MESSAGE: {:?}", msg); println!("WEBSOCKET MESSAGE: {:?}", msg);
match msg { match msg {
ws::Message::Ping(msg) => { ws::Message::Ping(msg) => {
self.hb = Instant::now(); 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(); let login: Login = serde_json::from_str(&data.to_string()).unwrap();
ctx.state() ctx.state()
.addr .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 => { UserOperation::Register => {
let register: Register = serde_json::from_str(&data.to_string()).unwrap(); 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) .into_actor(self)
.then(|res, _, ctx| { .then(|res, _, ctx| {
match res { 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"), _ => println!("Something is wrong"),
} }
fut::ok(()) fut::ok(())
}) })
.wait(ctx) .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)), _ => ctx.text(format!("!!! unknown command: {:?}", m)),
} }

View File

@ -8,6 +8,9 @@ pub extern crate actix;
pub extern crate actix_web; pub extern crate actix_web;
pub extern crate rand; pub extern crate rand;
pub extern crate strum; pub extern crate strum;
pub extern crate jsonwebtoken;
pub extern crate bcrypt;
pub extern crate regex;
#[macro_use] pub extern crate strum_macros; #[macro_use] pub extern crate strum_macros;
pub mod schema; pub mod schema;
@ -20,28 +23,28 @@ use diesel::pg::PgConnection;
use diesel::result::Error; use diesel::result::Error;
use dotenv::dotenv; use dotenv::dotenv;
use std::env; use std::env;
use regex::Regex;
pub trait Crud<T> { pub trait Crud<T> {
fn create(conn: &PgConnection, form: T) -> Result<Self, Error> where Self: Sized; fn create(conn: &PgConnection, form: &T) -> Result<Self, Error> where Self: Sized;
fn read(conn: &PgConnection, id: i32) -> Self; fn read(conn: &PgConnection, id: i32) -> Result<Self, Error> where Self: Sized;
fn update(conn: &PgConnection, id: i32, form: T) -> Self; fn update(conn: &PgConnection, id: i32, form: &T) -> Result<Self, Error> where Self: Sized;
fn delete(conn: &PgConnection, id: i32) -> usize; fn delete(conn: &PgConnection, id: i32) -> Result<usize, Error> where Self: Sized;
} }
pub trait Followable<T> { pub trait Followable<T> {
fn follow(conn: &PgConnection, form: T) -> Result<Self, Error> where Self: Sized; fn follow(conn: &PgConnection, form: &T) -> Result<Self, Error> where Self: Sized;
fn ignore(conn: &PgConnection, form: T) -> usize; fn ignore(conn: &PgConnection, form: &T) -> Result<usize, Error> where Self: Sized;
} }
pub trait Joinable<T> { pub trait Joinable<T> {
fn join(conn: &PgConnection, form: T) -> Result<Self, Error> where Self: Sized; fn join(conn: &PgConnection, form: &T) -> Result<Self, Error> where Self: Sized;
fn leave(conn: &PgConnection, form: T) -> usize; fn leave(conn: &PgConnection, form: &T) -> Result<usize, Error> where Self: Sized;
} }
pub trait Likeable<T> { pub trait Likeable<T> {
fn like(conn: &PgConnection, form: T) -> Result<Self, Error> where Self: Sized; fn like(conn: &PgConnection, form: &T) -> Result<Self, Error> where Self: Sized;
fn remove(conn: &PgConnection, form: T) -> usize; fn remove(conn: &PgConnection, form: &T) -> Result<usize, Error> where Self: Sized;
} }
pub fn establish_connection() -> PgConnection { pub fn establish_connection() -> PgConnection {
@ -61,7 +64,7 @@ impl Settings {
Settings { Settings {
db_url: env::var("DATABASE_URL") db_url: env::var("DATABASE_URL")
.expect("DATABASE_URL must be set"), .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 { fn api_endpoint(&self) -> String {
@ -78,11 +81,22 @@ pub fn naive_now() -> NaiveDateTime {
chrono::prelude::Utc::now().naive_utc() 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)] #[cfg(test)]
mod tests { mod tests {
use Settings; use {Settings, is_email_regex};
#[test] #[test]
fn test_api() { fn test_api() {
assert_eq!(Settings::get().api_endpoint(), "http://0.0.0.0/api/v1"); 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 rand::{rngs::ThreadRng, Rng};
use std::collections::{HashMap, HashSet}; use std::collections::{HashMap, HashSet};
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use bcrypt::{verify};
use {Crud,establish_connection}; use {Crud,establish_connection};
use actions::community::*;
#[derive(EnumString,ToString,Debug)] #[derive(EnumString,ToString,Debug)]
pub enum UserOperation { 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 /// Chat server sends this messages to session
#[derive(Message)] #[derive(Message)]
@ -66,14 +74,16 @@ pub struct Join {
pub name: String, pub name: String,
} }
#[derive(Message)]
#[derive(Serialize, Deserialize)] #[derive(Serialize, Deserialize)]
pub struct Login { pub struct Login {
pub username: String, pub username_or_email: String,
pub password: String pub password: String
} }
// #[derive(Message)] impl actix::Message for Login {
type Result = Result<LoginResponse, ErrorMessage>;
}
#[derive(Serialize, Deserialize)] #[derive(Serialize, Deserialize)]
pub struct Register { pub struct Register {
username: String, username: String,
@ -82,9 +92,31 @@ pub struct Register {
password_verify: String password_verify: String
} }
impl actix::Message for Register { #[derive(Serialize, Deserialize)]
type Result = String; 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 /// `ChatServer` manages chat rooms and responsible for coordinating chat
/// session. implementation is super primitive /// session. implementation is super primitive
pub struct ChatServer { pub struct ChatServer {
@ -233,10 +265,47 @@ impl Handler<Join> for ChatServer {
impl Handler<Login> for ChatServer { impl Handler<Login> for ChatServer {
type Result = (); type Result = MessageResult<Login>;
fn handle(&mut self, msg: Login, _: &mut Context<Self>) { fn handle(&mut self, msg: Login, _: &mut Context<Self>) -> Self::Result {
println!("{}", msg.password);
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::*; use actions::user::*;
let conn = establish_connection(); 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 // Register the new user
let user_form = UserForm { let user_form = UserForm {
name: &msg.username, name: msg.username,
email: msg.email.as_ref().map(|x| &**x), email: msg.email,
password_encrypted: &msg.password, password_encrypted: msg.password,
preferred_username: None, preferred_username: None,
updated: 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 // 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, "engineStrict": true,
"dependencies": { "dependencies": {
"@types/js-cookie": "^2.2.1",
"classcat": "^1.1.3", "classcat": "^1.1.3",
"dotenv": "^6.1.0", "dotenv": "^6.1.0",
"inferno": "^7.0.1", "inferno": "^7.0.1",
"inferno-router": "^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": { "devDependencies": {
"fuse-box": "3.1.3", "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 { Component, linkEvent } from 'inferno';
import { Subscription } from "rxjs";
import { LoginForm, RegisterForm } from '../interfaces'; import { retryWhen, delay, take } from 'rxjs/operators';
import { WebSocketService } from '../services'; import { LoginForm, RegisterForm, UserOperation } from '../interfaces';
import { WebSocketService, UserService } from '../services';
import { msgOp } from '../utils';
interface State { interface State {
loginForm: LoginForm; loginForm: LoginForm;
@ -10,24 +12,36 @@ interface State {
let emptyState: State = { let emptyState: State = {
loginForm: { loginForm: {
username: null, username_or_email: undefined,
password: null password: undefined
}, },
registerForm: { registerForm: {
username: null, username: undefined,
password: null, password: undefined,
password_verify: null password_verify: undefined
} }
} }
export class Login extends Component<any, State> { export class Login extends Component<any, State> {
private subscription: Subscription;
constructor(props, context) { constructor(props, context) {
super(props, context); super(props, context);
this.state = emptyState; 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() { render() {
return ( return (
<div class="container"> <div class="container">
@ -51,7 +65,7 @@ export class Login extends Component<any, State> {
<div class="form-group row"> <div class="form-group row">
<label class="col-sm-2 col-form-label">Email or Username</label> <label class="col-sm-2 col-form-label">Email or Username</label>
<div class="col-sm-10"> <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> </div>
<div class="form-group row"> <div class="form-group row">
@ -108,38 +122,55 @@ export class Login extends Component<any, State> {
} }
handleLoginSubmit(i: Login, event) { handleLoginSubmit(i: Login, event) {
console.log(i.state);
event.preventDefault(); event.preventDefault();
WebSocketService.Instance.login(i.state.loginForm); WebSocketService.Instance.login(i.state.loginForm);
} }
handleLoginUsernameChange(i: Login, event) { 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) { handleLoginPasswordChange(i: Login, event) {
i.state.loginForm.password = event.target.value; i.state.loginForm.password = event.target.value;
i.setState(i.state);
} }
handleRegisterSubmit(i: Login, event) { handleRegisterSubmit(i: Login, event) {
console.log(i.state);
event.preventDefault(); event.preventDefault();
WebSocketService.Instance.register(i.state.registerForm); WebSocketService.Instance.register(i.state.registerForm);
} }
handleRegisterUsernameChange(i: Login, event) { handleRegisterUsernameChange(i: Login, event) {
i.state.registerForm.username = event.target.value; i.state.registerForm.username = event.target.value;
i.setState(i.state);
} }
handleRegisterEmailChange(i: Login, event) { handleRegisterEmailChange(i: Login, event) {
i.state.registerForm.email = event.target.value; i.state.registerForm.email = event.target.value;
i.setState(i.state);
} }
handleRegisterPasswordChange(i: Login, event) { handleRegisterPasswordChange(i: Login, event) {
i.state.registerForm.password = event.target.value; i.state.registerForm.password = event.target.value;
i.setState(i.state);
} }
handleRegisterPasswordVerifyChange(i: Login, event) { handleRegisterPasswordVerifyChange(i: Login, event) {
i.state.registerForm.password_verify = event.target.value; 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 { Component, linkEvent } from 'inferno';
import { Link } from 'inferno-router'; import { Link } from 'inferno-router';
import { repoUrl } from '../utils'; import { repoUrl } from '../utils';
import { UserService } from '../services';
export class Navbar extends Component<any, any> { export class Navbar extends Component<any, any> {
constructor(props, context) { constructor(props, context) {
super(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() { render() {
return ( return (
<div class="sticky-top">{this.navbar()}</div> <div>{this.navbar()}</div>
) )
} }
// TODO class active corresponding to current page // TODO class active corresponding to current page
// TODO toggle css collapse
navbar() { navbar() {
return ( return (
<nav class="navbar navbar-light bg-light p-0 px-3 shadow"> <nav class="navbar navbar-expand-sm navbar-light bg-light p-0 px-3 shadow">
<a class="navbar-brand mx-1" href="#"> <a class="navbar-brand" href="#">rrf</a>
rrf <button class="navbar-toggler" type="button" data-toggle="collapse" data-target="#navbarNav" aria-controls="navbarNav" aria-expanded="false" aria-label="Toggle navigation">
</a> <span class="navbar-toggler-icon"></span>
<ul class="navbar-nav mr-auto"> </button>
<li class="nav-item"> <div class="collapse navbar-collapse">
<a class="nav-item nav-link" href={repoUrl}>github</a> <ul class="navbar-nav mr-auto">
</li> <li class="nav-item">
</ul> <a class="nav-link" href={repoUrl}>github</a>
<ul class="navbar-nav ml-auto mr-2"> </li>
<li class="nav-item"> <li class="nav-item">
<Link class="nav-item nav-link" to="/login">Login</Link> <Link class="nav-link" to="/create_post">Create Post</Link>
</li> </li>
</ul> <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> </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 { Navbar } from './components/navbar';
import { Home } from './components/home'; import { Home } from './components/home';
import { Login } from './components/login'; import { Login } from './components/login';
import { CreatePost } from './components/create-post';
import { CreateCommunity } from './components/create-community';
import './main.css'; import './main.css';
import { WebSocketService } from './services'; import { WebSocketService, UserService } from './services';
const container = document.getElementById('app'); const container = document.getElementById('app');
@ -16,6 +18,7 @@ class Index extends Component<any, any> {
constructor(props, context) { constructor(props, context) {
super(props, context); super(props, context);
WebSocketService.Instance; WebSocketService.Instance;
UserService.Instance;
} }
render() { render() {
@ -26,6 +29,8 @@ class Index extends Component<any, any> {
<Switch> <Switch>
<Route exact path="/" component={Home} /> <Route exact path="/" component={Home} />
<Route path={`/login`} component={Login} /> <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={`/search/:type_/:q/:page`} component={Search} />
<Route path={`/submit`} component={Submit} /> <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; username: string;
}
export interface LoginForm {
username_or_email: string;
password: string; password: string;
} }
export interface RegisterForm { export interface RegisterForm {
username: string; username: string;
email?: string; email?: string;
@ -9,6 +19,14 @@ export interface RegisterForm {
password_verify: string; password_verify: string;
} }
export enum UserOperation { export interface CommunityForm {
Login, Register 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 repoUrl = 'https://github.com/dessalines/rust-reddit-fediverse';
export let wsUri = (window.location.protocol=='https:'&&'wss://'||'ws://')+window.location.host + '/service/ws/'; 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: dependencies:
regenerator-runtime "^0.12.0" 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: abbrev@1:
version "1.1.1" version "1.1.1"
resolved "https://registry.yarnpkg.com/abbrev/-/abbrev-1.1.1.tgz#f8f2c887ad10bf67f634f005b6987fed3179aac8" 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" resolved "https://registry.yarnpkg.com/isstream/-/isstream-0.1.2.tgz#47e63f7af55afa6f92e1500e690eb8b8529c099a"
integrity sha1-R+Y/evVa+m+S4VAOaQ64uFKcCZo= 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": "js-tokens@^3.0.0 || ^4.0.0":
version "4.0.0" version "4.0.0"
resolved "https://registry.yarnpkg.com/js-tokens/-/js-tokens-4.0.0.tgz#19203fb59991df98e3a287050d4647cdeaf32499" 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" json-schema "0.2.3"
verror "1.10.0" 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: kind-of@^3.0.2, kind-of@^3.0.3, kind-of@^3.2.0:
version "3.2.2" version "3.2.2"
resolved "https://registry.yarnpkg.com/kind-of/-/kind-of-3.2.2.tgz#31ea21a734bab9bbb0f32466d893aea51e4a3c64" 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" resolved "https://registry.yarnpkg.com/rx-lite/-/rx-lite-4.0.8.tgz#0b1e11af8bc44836f04a6407e92da42467b79444"
integrity sha1-Cx4Rr4vESDbwSmQH6S2kJGe3lEQ= 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: 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" version "5.1.2"
resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.1.2.tgz#991ec69d296e0313747d59bdfd2b745c35f8828d" 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" resolved "https://registry.yarnpkg.com/ts-transform-inferno/-/ts-transform-inferno-4.0.2.tgz#06b9be45edf874ba7a6ebfb6107ba782509c6afe"
integrity sha512-CZb4+w/2l2zikPZ/c51fi3n+qnR2HCEfAS73oGQB80aqRLffkZqm25kYYTMmqUW2+oVfs4M5AZa0z14cvxlQ5w== integrity sha512-CZb4+w/2l2zikPZ/c51fi3n+qnR2HCEfAS73oGQB80aqRLffkZqm25kYYTMmqUW2+oVfs4M5AZa0z14cvxlQ5w==
tslib@^1.8.0: tslib@^1.8.0, tslib@^1.9.0:
version "1.9.3" version "1.9.3"
resolved "https://registry.yarnpkg.com/tslib/-/tslib-1.9.3.tgz#d7e4dd79245d85428c4d7e4822a79917954ca286" resolved "https://registry.yarnpkg.com/tslib/-/tslib-1.9.3.tgz#d7e4dd79245d85428c4d7e4822a79917954ca286"
integrity sha512-4krF8scpejhaOgqzBEcGM7yDIEfi0/8+8zDRZhNZZ2kjmHJ4hv3zCbQWxoJGz1iw5U0Jl0nma13xzHXcncMavQ== integrity sha512-4krF8scpejhaOgqzBEcGM7yDIEfi0/8+8zDRZhNZZ2kjmHJ4hv3zCbQWxoJGz1iw5U0Jl0nma13xzHXcncMavQ==