From 15095dc57463af964763327a0440fe01bcdc74b1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adrian=20Wo=C5=BAniak?= Date: Thu, 16 Apr 2020 16:11:19 +0200 Subject: [PATCH] Handle sign in process --- Cargo.lock | 2 + jirs-client/src/lib.rs | 7 ++- jirs-client/src/login.rs | 55 +++++++++++++++++------ jirs-client/src/model.rs | 1 + jirs-client/src/shared/styled_input.rs | 8 ---- jirs-client/src/shared/styled_textarea.rs | 15 ++++--- jirs-data/src/lib.rs | 3 ++ jirs-server/Cargo.toml | 2 +- jirs-server/seed.sql | 15 +++++-- jirs-server/src/db/tokens.rs | 33 +++++++++++--- jirs-server/src/db/users.rs | 6 ++- jirs-server/src/models.rs | 1 + jirs-server/src/ws/auth.rs | 35 +++++++++++++++ jirs-server/src/ws/mod.rs | 19 ++++++-- jirs-server/src/ws/users.rs | 19 +------- 15 files changed, 160 insertions(+), 61 deletions(-) create mode 100644 jirs-server/src/ws/auth.rs diff --git a/Cargo.lock b/Cargo.lock index 7a4a3b0b..e0503ca4 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2245,7 +2245,9 @@ version = "0.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9fde2f6a4bea1d6e007c4ad38c6839fa71cbb63b6dbf5b595aa38dc9b1093c11" dependencies = [ + "rand 0.7.3", "serde", + "sha1", ] [[package]] diff --git a/jirs-client/src/lib.rs b/jirs-client/src/lib.rs index 8ecb3e45..4b1ff323 100644 --- a/jirs-client/src/lib.rs +++ b/jirs-client/src/lib.rs @@ -139,6 +139,7 @@ pub enum Msg { AuthTokenStored, AuthTokenErased, SignInRequest, + BindClientRequest, StyledSelectChanged(FieldId, StyledSelectChange), @@ -189,11 +190,13 @@ fn update(msg: Msg, model: &mut model::Model, orders: &mut impl Orders) { } match &msg { Msg::AuthTokenStored => { - seed::push_route(vec!["/dashboard"]); + seed::push_route(vec!["dashboard"]); + orders.skip().send_msg(Msg::ChangePage(Page::Project)); return; } Msg::AuthTokenErased => { - seed::push_route(vec!["/login"]); + seed::push_route(vec!["login"]); + orders.skip().send_msg(Msg::ChangePage(Page::Login)); return; } Msg::ChangePage(page) => { diff --git a/jirs-client/src/login.rs b/jirs-client/src/login.rs index 4b921304..16462b4d 100644 --- a/jirs-client/src/login.rs +++ b/jirs-client/src/login.rs @@ -6,12 +6,14 @@ use crate::shared::styled_button::StyledButton; use crate::shared::styled_field::StyledField; use crate::shared::styled_form::StyledForm; use crate::shared::styled_icon::{Icon, StyledIcon}; -use crate::shared::styled_input::StyledInput; -use crate::shared::{outer_layout, ToNode}; +use crate::shared::styled_textarea::StyledTextarea; +use crate::shared::{outer_layout, write_auth_token, ToNode}; use crate::{model, FieldId, LoginFieldId, Msg}; use jirs_data::WsMsg; +use std::str::FromStr; +use uuid::Uuid; -pub fn update(msg: Msg, model: &mut model::Model, _orders: &mut impl Orders) { +pub fn update(msg: Msg, model: &mut model::Model, orders: &mut impl Orders) { if model.page != Page::Login { return; } @@ -33,15 +35,38 @@ pub fn update(msg: Msg, model: &mut model::Model, _orders: &mut impl Orders Msg::InputChanged(FieldId::Login(LoginFieldId::Email), value) => { page.email = value; } + Msg::InputChanged(FieldId::Login(LoginFieldId::Token), value) => { + page.token = value; + } Msg::SignInRequest => { send_ws_msg(WsMsg::AuthenticateRequest( page.email.clone(), page.username.clone(), )); } + Msg::BindClientRequest => { + let bind_token: uuid::Uuid = match Uuid::from_str(page.token.as_str()) { + Ok(token) => token, + Err(error) => { + error!(error); + return; + } + }; + send_ws_msg(WsMsg::BindTokenCheck(bind_token)); + } Msg::WsMsg(WsMsg::AuthenticateSuccess) => { page.login_success = true; } + Msg::WsMsg(WsMsg::BindTokenOk(access_token)) => { + match write_auth_token(Some(access_token)) { + Ok(msg) => { + orders.skip().send_msg(msg); + } + Err(e) => { + error!(e); + } + } + } _ => (), }; } @@ -52,10 +77,11 @@ pub fn view(model: &model::Model) -> Node { _ => return empty![], }; - let username = StyledInput::build(FieldId::Login(LoginFieldId::Username)) - .valid(true) + let username = StyledTextarea::build() + .one_line() + .update_on(Ev::Change) .value(page.username.as_str()) - .build() + .build(FieldId::Login(LoginFieldId::Username)) .into_node(); let username_field = StyledField::build() .label("Username") @@ -63,11 +89,11 @@ pub fn view(model: &model::Model) -> Node { .build() .into_node(); - let email = StyledInput::build(FieldId::Login(LoginFieldId::Email)) - .valid(true) + let email = StyledTextarea::build() + .one_line() + .update_on(Ev::Change) .value(page.email.as_str()) - .input_type("email") - .build() + .build(FieldId::Login(LoginFieldId::Email)) .into_node(); let email_field = StyledField::build() .label("E-Mail") @@ -100,10 +126,11 @@ pub fn view(model: &model::Model) -> Node { span!["Why I don't see password?"] ]; - let token = StyledInput::build(FieldId::Login(LoginFieldId::Token)) - .valid(true) + let token = StyledTextarea::build() + .one_line() + .update_on(Ev::Input) .value(page.token.as_str()) - .build() + .build(FieldId::Login(LoginFieldId::Token)) .into_node(); let token_field = StyledField::build() .label("Single use token") @@ -113,7 +140,7 @@ pub fn view(model: &model::Model) -> Node { let submit_token = StyledButton::build() .primary() .text("Authorize") - .on_click(mouse_ev(Ev::Click, |_| Msg::SignInRequest)) + .on_click(mouse_ev(Ev::Click, |_| Msg::BindClientRequest)) .build() .into_node(); let submit_token_field = StyledField::build().input(submit_token).build().into_node(); diff --git a/jirs-client/src/model.rs b/jirs-client/src/model.rs index d17edbd9..db3593b1 100644 --- a/jirs-client/src/model.rs +++ b/jirs-client/src/model.rs @@ -232,6 +232,7 @@ pub struct LoginPage { pub email: String, pub token: String, pub login_success: bool, + pub bad_token: String, } #[derive(Debug)] diff --git a/jirs-client/src/shared/styled_input.rs b/jirs-client/src/shared/styled_input.rs index 9122a6ca..66b6b1ec 100644 --- a/jirs-client/src/shared/styled_input.rs +++ b/jirs-client/src/shared/styled_input.rs @@ -53,14 +53,6 @@ impl StyledInputBuilder { self } - pub fn input_type(mut self, input_type: S) -> Self - where - S: Into, - { - self.input_type = Some(input_type.into()); - self - } - pub fn build(self) -> StyledInput { StyledInput { id: self.id, diff --git a/jirs-client/src/shared/styled_textarea.rs b/jirs-client/src/shared/styled_textarea.rs index cca11c47..1ae4ca22 100644 --- a/jirs-client/src/shared/styled_textarea.rs +++ b/jirs-client/src/shared/styled_textarea.rs @@ -40,6 +40,11 @@ pub struct StyledTextareaBuilder { } impl StyledTextareaBuilder { + #[inline] + pub fn one_line(self) -> Self { + self.disable_auto_resize().height(39).max_height(39) + } + #[inline] pub fn height(mut self, height: usize) -> Self { self.height = Some(height); @@ -162,12 +167,12 @@ pub fn render(values: StyledTextarea) -> Node { Msg::NoOp }); handlers.push(resize_handler); - let text_input_handler = input_ev(update_event, move |value| Msg::InputChanged(id, value)); + let text_input_handler = ev(update_event, move |event| { + let target = event.target().unwrap(); + let text = seed::to_textarea(&target).value(); + Msg::InputChanged(id, text) + }); handlers.push(text_input_handler); - handlers.push(keyboard_ev(Ev::Input, |ev| { - ev.stop_propagation(); - Msg::NoOp - })); class_list.push("textAreaInput".to_string()); diff --git a/jirs-data/src/lib.rs b/jirs-data/src/lib.rs index 2d55f497..37eeb14c 100644 --- a/jirs-data/src/lib.rs +++ b/jirs-data/src/lib.rs @@ -474,6 +474,9 @@ pub enum WsMsg { AuthorizeExpired, AuthenticateRequest(EmailString, UsernameString), AuthenticateSuccess, + BindTokenCheck(Uuid), + BindTokenBad, + BindTokenOk(Uuid), // project page ProjectRequest, diff --git a/jirs-server/Cargo.toml b/jirs-server/Cargo.toml index 9e524b95..f06cba64 100644 --- a/jirs-server/Cargo.toml +++ b/jirs-server/Cargo.toml @@ -32,7 +32,7 @@ bincode = "1.2.1" time = { version = "0.1" } url = { version = "2.1.0" } percent-encoding = { version = "2.1.0" } -uuid = { version = ">=0.7.0, <0.9.0", features = ["serde"] } +uuid = { version = ">=0.7.0, <0.9.0", features = ["serde", "v4", "v5"] } ipnetwork = { version = ">=0.12.2, <0.17.0" } num-bigint = { version = ">=0.1.41, <0.3" } num-traits = { version = "0.2" } diff --git a/jirs-server/seed.sql b/jirs-server/seed.sql index c0badd1f..1afa724b 100644 --- a/jirs-server/seed.sql +++ b/jirs-server/seed.sql @@ -1,10 +1,19 @@ insert into projects (name) values ('initial'), ('second'), ('third'); insert into users (project_id, email, name, avatar_url) values ( - 1, 'john@example.com', 'John Doe', 'http://cdn.onlinewebfonts.com/svg/img_553934.png + 1, + 'john@example.com', + 'John Doe', + 'http://cdn.onlinewebfonts.com/svg/img_553934.png' ), ( - 1, 'kate@exampe.com', 'Kate Snow', 'http://www.asthmamd.org/images/icon_user_6.png + 1, + 'kate@exampe.com', + 'Kate Snow', + 'http://www.asthmamd.org/images/icon_user_6.png' ), ( - 1, 'mike@example.com', 'Mike Keningham', 'https://cdn0.iconfinder.com/data/icons/user-pictures/100/matureman1-512.png' + 1, + 'mike@example.com', + 'Mike Keningham', + 'https://cdn0.iconfinder.com/data/icons/user-pictures/100/matureman1-512.png' ); insert into tokens (user_id, access_token, refresh_token) values (1, uuid_generate_v4(), uuid_generate_v4() ); insert into issues( diff --git a/jirs-server/src/db/tokens.rs b/jirs-server/src/db/tokens.rs index 8d082282..26f45310 100644 --- a/jirs-server/src/db/tokens.rs +++ b/jirs-server/src/db/tokens.rs @@ -7,6 +7,7 @@ use jirs_data::UserId; use crate::db::DbExecutor; use crate::errors::ServiceErrors; +use crate::models::{Token, TokenForm}; #[derive(Serialize, Deserialize, Debug)] pub struct FindBindToken { @@ -28,11 +29,17 @@ impl Handler for DbExecutor { .map_err(|_| ServiceErrors::DatabaseConnectionLost)?; let token: crate::models::Token = tokens - .filter(bind_token.eq(msg.token)) + .filter(bind_token.eq(Some(msg.token))) .first(conn) - .map_err(|_e| { - ServiceErrors::RecordNotFound(format!("token for {}", msg.access_token)) - })?; + .map_err(|_e| ServiceErrors::RecordNotFound(format!("token for {}", msg.token)))?; + + let erase_value: Option = None; + diesel::update(tokens.find(token.id)) + .set(bind_token.eq(erase_value)) + .execute(conn) + .map_err(|_| ServiceErrors::DatabaseConnectionLost)?; + + Ok(token) } } @@ -49,10 +56,26 @@ impl Handler for DbExecutor { type Result = Result; fn handle(&mut self, msg: CreateBindToken, _: &mut Self::Context) -> Self::Result { - use crate::schema::tokens::dsl::{access_token, tokens}; + use crate::schema::tokens::dsl::tokens; let conn = &self .0 .get() .map_err(|_| ServiceErrors::DatabaseConnectionLost)?; + + let access_token = Uuid::new_v4(); + let refresh_token = Uuid::new_v4(); + let bind_token = Some(Uuid::new_v4()); + + let form = TokenForm { + user_id: msg.user_id, + access_token, + refresh_token, + bind_token, + }; + let row: Token = diesel::insert_into(tokens) + .values(form) + .get_result(conn) + .map_err(|_| ServiceErrors::RecordNotFound("issue comments".to_string()))?; + Ok(row) } } diff --git a/jirs-server/src/db/users.rs b/jirs-server/src/db/users.rs index b1f9043e..a300ee72 100644 --- a/jirs-server/src/db/users.rs +++ b/jirs-server/src/db/users.rs @@ -5,7 +5,7 @@ use actix::{Handler, Message}; use diesel::prelude::*; use serde::{Deserialize, Serialize}; -#[derive(Serialize, Deserialize)] +#[derive(Serialize, Deserialize, Debug)] pub struct FindUser { pub name: String, pub email: String, @@ -30,7 +30,9 @@ impl Handler for DbExecutor { .filter(email.eq(msg.email.as_str())) .filter(name.eq(msg.name.as_str())) .first(conn) - .map_err(|_| ServiceErrors::RecordNotFound("project users".to_string()))?; + .map_err(|_| { + ServiceErrors::RecordNotFound(format!("user {} {}", msg.name, msg.email)) + })?; Ok(row) } } diff --git a/jirs-server/src/models.rs b/jirs-server/src/models.rs index c30f0c1e..757d90a1 100644 --- a/jirs-server/src/models.rs +++ b/jirs-server/src/models.rs @@ -236,4 +236,5 @@ pub struct TokenForm { pub user_id: i32, pub access_token: Uuid, pub refresh_token: Uuid, + pub bind_token: Option, } diff --git a/jirs-server/src/ws/auth.rs b/jirs-server/src/ws/auth.rs new file mode 100644 index 00000000..733dbe9f --- /dev/null +++ b/jirs-server/src/ws/auth.rs @@ -0,0 +1,35 @@ +use crate::db::tokens::CreateBindToken; +use crate::db::users::FindUser; +use crate::db::DbExecutor; +use crate::ws::WsResult; +use actix::Addr; +use actix_web::web::Data; +use jirs_data::WsMsg; + +pub async fn authenticate(db: &Data>, name: String, email: String) -> WsResult { + // TODO check attempt number, allow only 5 times per day + let user = match db.send(FindUser { name, email }).await { + Ok(Ok(user)) => user, + Ok(Err(e)) => { + error!("{:?}", e); + return Ok(None); + } + Err(e) => { + error!("{:?}", e); + return Ok(None); + } + }; + let _token = match db.send(CreateBindToken { user_id: user.id }).await { + Ok(Ok(token)) => token, + Ok(Err(e)) => { + error!("{:?}", e); + return Ok(None); + } + Err(e) => { + error!("{:?}", e); + return Ok(None); + } + }; + // TODO send email somehow + Ok(Some(WsMsg::AuthenticateSuccess)) +} diff --git a/jirs-server/src/ws/mod.rs b/jirs-server/src/ws/mod.rs index 9f7c23dd..61816edd 100644 --- a/jirs-server/src/ws/mod.rs +++ b/jirs-server/src/ws/mod.rs @@ -6,8 +6,10 @@ use actix_web_actors::ws; use jirs_data::WsMsg; use crate::db::authorize_user::AuthorizeUser; +use crate::db::tokens::FindBindToken; use crate::db::DbExecutor; +pub mod auth; pub mod comments; pub mod issues; pub mod projects; @@ -82,9 +84,10 @@ impl WebSocketActor { ))?, // auth - WsMsg::AuthorizeRequest(uuid) => block_on(self.authorize(uuid))?, + WsMsg::AuthorizeRequest(uuid) => block_on(self.check_auth_token(uuid))?, + WsMsg::BindTokenCheck(uuid) => block_on(self.check_bind_token(uuid))?, WsMsg::AuthenticateRequest(email, name) => { - block_on(users::authenticate(&self.db, name, email))? + block_on(auth::authenticate(&self.db, name, email))? } // users @@ -129,7 +132,7 @@ impl WebSocketActor { Ok(msg) } - async fn authorize(&mut self, token: uuid::Uuid) -> WsResult { + async fn check_auth_token(&mut self, token: uuid::Uuid) -> WsResult { let m = match self .db .send(AuthorizeUser { @@ -149,6 +152,16 @@ impl WebSocketActor { }; Ok(m) } + + async fn check_bind_token(&mut self, bind_token: uuid::Uuid) -> WsResult { + let token: crate::models::Token = + match self.db.send(FindBindToken { token: bind_token }).await { + Ok(Ok(token)) => token, + Ok(Err(_)) => return Ok(Some(WsMsg::BindTokenBad)), + _ => return Ok(None), + }; + Ok(Some(WsMsg::BindTokenOk(token.access_token))) + } } impl StreamHandler> for WebSocketActor { diff --git a/jirs-server/src/ws/users.rs b/jirs-server/src/ws/users.rs index a9fab16f..24089bc5 100644 --- a/jirs-server/src/ws/users.rs +++ b/jirs-server/src/ws/users.rs @@ -3,27 +3,10 @@ use actix_web::web::Data; use jirs_data::WsMsg; -use crate::db::users::{FindUser, LoadProjectUsers}; +use crate::db::users::LoadProjectUsers; use crate::db::DbExecutor; use crate::ws::{current_user, WsResult}; -pub async fn authenticate(db: &Data>, name: String, email: String) -> WsResult { - // TODO check attempt number, allow only 5 times per day - let _user = match db.send(FindUser { name, email }).await { - Ok(Ok(user)) => user, - Ok(Err(e)) => { - error!("{:?}", e); - return Ok(None); - } - Err(e) => { - error!("{:?}", e); - return Ok(None); - } - }; - // TODO send email somehow - Ok(Some(WsMsg::AuthenticateSuccess)) -} - pub async fn load_project_users( db: &Data>, user: &Option,