diff --git a/jirs-client/js/css/login.css b/jirs-client/js/css/login.css index 8f3a9182..0c6b45b2 100644 --- a/jirs-client/js/css/login.css +++ b/jirs-client/js/css/login.css @@ -28,7 +28,7 @@ } #login > .styledForm > .formElement > .noPasswordSection > .styledIcon { - margin-right: 15px; + margin-right: 5px; line-height: 32px; } diff --git a/jirs-client/src/lib.rs b/jirs-client/src/lib.rs index 98176d9b..8ecb3e45 100644 --- a/jirs-client/src/lib.rs +++ b/jirs-client/src/lib.rs @@ -6,7 +6,6 @@ use jirs_data::*; use crate::api::send_ws_msg; use crate::model::{ModalType, Model, Page}; -use crate::shared::read_auth_token; use crate::shared::styled_editor::Mode as TabMode; use crate::shared::styled_select::StyledSelectChange; @@ -59,7 +58,9 @@ pub enum ProjectSettingsFieldId { #[derive(Clone, Debug, PartialOrd, PartialEq, Hash)] pub enum LoginFieldId { + Username, Email, + Token, } #[derive(Clone, Debug, PartialOrd, PartialEq, Hash)] @@ -109,6 +110,8 @@ impl std::fmt::Display for FieldId { }, FieldId::Login(sub) => match sub { LoginFieldId::Email => f.write_str("login-email"), + LoginFieldId::Username => f.write_str("login-username"), + LoginFieldId::Token => f.write_str("login-token"), }, } } @@ -135,6 +138,7 @@ pub enum Msg { // Auth Token AuthTokenStored, AuthTokenErased, + SignInRequest, StyledSelectChanged(FieldId, StyledSelectChange), diff --git a/jirs-client/src/login.rs b/jirs-client/src/login.rs index e5011a8f..4b921304 100644 --- a/jirs-client/src/login.rs +++ b/jirs-client/src/login.rs @@ -1,36 +1,72 @@ use seed::{prelude::*, *}; -use crate::model::Page; +use crate::api::send_ws_msg; +use crate::model::{LoginPage, Page, PageContent}; +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::{model, FieldId, LoginFieldId, Msg}; +use jirs_data::WsMsg; pub fn update(msg: Msg, model: &mut model::Model, _orders: &mut impl Orders) { if model.page != Page::Login { return; } + if msg == Msg::ChangePage(Page::Login) { + model.page_content = PageContent::Login(LoginPage::default()); + return; + } + + let page = match &mut model.page_content { + PageContent::Login(page) => page, + _ => return, + }; + match msg { + Msg::InputChanged(FieldId::Login(LoginFieldId::Username), value) => { + page.username = value; + } + Msg::InputChanged(FieldId::Login(LoginFieldId::Email), value) => { + page.email = value; + } + Msg::SignInRequest => { + send_ws_msg(WsMsg::AuthenticateRequest( + page.email.clone(), + page.username.clone(), + )); + } + Msg::WsMsg(WsMsg::AuthenticateSuccess) => { + page.login_success = true; + } _ => (), }; } pub fn view(model: &model::Model) -> Node { - let login = StyledInput::build(FieldId::Login(LoginFieldId::Email)) + let page = match &model.page_content { + PageContent::Login(page) => page, + _ => return empty![], + }; + + let username = StyledInput::build(FieldId::Login(LoginFieldId::Username)) .valid(true) + .value(page.username.as_str()) .build() .into_node(); - let login_field = StyledField::build() - .label("Login") - .input(login) + let username_field = StyledField::build() + .label("Username") + .input(username) .build() .into_node(); let email = StyledInput::build(FieldId::Login(LoginFieldId::Email)) .valid(true) + .value(page.email.as_str()) + .input_type("email") .build() .into_node(); let email_field = StyledField::build() @@ -39,6 +75,18 @@ pub fn view(model: &model::Model) -> Node { .build() .into_node(); + let submit = if page.login_success { + StyledButton::build().success().text("✓") + } else { + StyledButton::build() + .primary() + .text("Sign In") + .on_click(mouse_ev(Ev::Click, |_| Msg::SignInRequest)) + } + .build() + .into_node(); + let submit_field = StyledField::build().input(submit).build().into_node(); + let help_icon = StyledIcon::build(Icon::Help) .add_class("noPasswordHelp") .size(22) @@ -52,11 +100,32 @@ pub fn view(model: &model::Model) -> Node { span!["Why I don't see password?"] ]; + let token = StyledInput::build(FieldId::Login(LoginFieldId::Token)) + .valid(true) + .value(page.token.as_str()) + .build() + .into_node(); + let token_field = StyledField::build() + .label("Single use token") + .input(token) + .build() + .into_node(); + let submit_token = StyledButton::build() + .primary() + .text("Authorize") + .on_click(mouse_ev(Ev::Click, |_| Msg::SignInRequest)) + .build() + .into_node(); + let submit_token_field = StyledField::build().input(submit_token).build().into_node(); + let form = StyledForm::build() .heading("Sign In to your account") - .add_field(login_field) + .add_field(username_field) .add_field(email_field) + .add_field(submit_field) .add_field(no_pass_section) + .add_field(token_field) + .add_field(submit_token_field) .build() .into_node(); diff --git a/jirs-client/src/model.rs b/jirs-client/src/model.rs index 6def4075..d17edbd9 100644 --- a/jirs-client/src/model.rs +++ b/jirs-client/src/model.rs @@ -226,8 +226,17 @@ impl ProjectSettingsPage { } } +#[derive(Debug, Default)] +pub struct LoginPage { + pub username: String, + pub email: String, + pub token: String, + pub login_success: bool, +} + #[derive(Debug)] pub enum PageContent { + Login(LoginPage), Project(ProjectPage), ProjectSettings(ProjectSettingsPage), } diff --git a/jirs-client/src/shared/styled_button.rs b/jirs-client/src/shared/styled_button.rs index a3a62dfa..2e244936 100644 --- a/jirs-client/src/shared/styled_button.rs +++ b/jirs-client/src/shared/styled_button.rs @@ -47,9 +47,9 @@ impl StyledButtonBuilder { self.variant(Variant::Primary) } - // pub fn success(self) -> Self { - // self.variant(Variant::Success) - // } + pub fn success(self) -> Self { + self.variant(Variant::Success) + } // pub fn danger(self) -> Self { // self.variant(Variant::Danger) diff --git a/jirs-client/src/shared/styled_input.rs b/jirs-client/src/shared/styled_input.rs index ae05775f..9122a6ca 100644 --- a/jirs-client/src/shared/styled_input.rs +++ b/jirs-client/src/shared/styled_input.rs @@ -10,6 +10,7 @@ pub struct StyledInput { icon: Option, valid: bool, value: Option, + input_type: Option, } impl StyledInput { @@ -19,6 +20,7 @@ impl StyledInput { icon: None, valid: None, value: None, + input_type: None, } } } @@ -29,6 +31,7 @@ pub struct StyledInputBuilder { icon: Option, valid: Option, value: Option, + input_type: Option, } impl StyledInputBuilder { @@ -50,12 +53,21 @@ 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, icon: self.icon, valid: self.valid.unwrap_or_default(), value: self.value, + input_type: self.input_type, } } } @@ -72,6 +84,7 @@ pub fn render(values: StyledInput) -> Node { icon, valid, value, + input_type, } = values; let mut wrapper_class_list = vec!["styledInput".to_string(), format!("{}", id)]; @@ -98,7 +111,6 @@ pub fn render(values: StyledInput) -> Node { .dyn_ref::() .unwrap() .value(); - log!("asd"); Msg::InputChanged(field_id, value) }); @@ -109,6 +121,7 @@ pub fn render(values: StyledInput) -> Node { attrs![ At::Class => input_class_list.join(" "), At::Value => value.unwrap_or_default(), + At::Type => input_type.unwrap_or_else(|| "text".to_string()), ], change_handler, ], diff --git a/jirs-client/src/ws/mod.rs b/jirs-client/src/ws/mod.rs index 47f4ec41..b083abad 100644 --- a/jirs-client/src/ws/mod.rs +++ b/jirs-client/src/ws/mod.rs @@ -26,8 +26,6 @@ pub fn update(msg: &Msg, model: &mut model::Model, orders: &mut impl Orders model.user = Some(user.clone()); } Msg::WsMsg(WsMsg::AuthorizeExpired) => { - use seed::*; - error!("Session expired"); if let Ok(msg) = write_auth_token(None) { orders.skip().send_msg(msg); } diff --git a/jirs-data/src/lib.rs b/jirs-data/src/lib.rs index ac75b33e..2d55f497 100644 --- a/jirs-data/src/lib.rs +++ b/jirs-data/src/lib.rs @@ -22,6 +22,8 @@ pub type ProjectId = i32; pub type UserId = i32; pub type CommentId = i32; pub type TokenId = i32; +pub type EmailString = String; +pub type UsernameString = String; #[cfg_attr(feature = "backend", derive(FromSqlRow, AsExpression))] #[cfg_attr(feature = "backend", sql_type = "IssueTypeType")] @@ -470,6 +472,8 @@ pub enum WsMsg { AuthorizeRequest(Uuid), AuthorizeLoaded(Result), AuthorizeExpired, + AuthenticateRequest(EmailString, UsernameString), + AuthenticateSuccess, // project page ProjectRequest, diff --git a/jirs-server/src/db/users.rs b/jirs-server/src/db/users.rs index d9dc1d0b..b1f9043e 100644 --- a/jirs-server/src/db/users.rs +++ b/jirs-server/src/db/users.rs @@ -5,6 +5,36 @@ use actix::{Handler, Message}; use diesel::prelude::*; use serde::{Deserialize, Serialize}; +#[derive(Serialize, Deserialize)] +pub struct FindUser { + pub name: String, + pub email: String, +} + +impl Message for FindUser { + type Result = Result; +} + +impl Handler for DbExecutor { + type Result = Result; + + fn handle(&mut self, msg: FindUser, _ctx: &mut Self::Context) -> Self::Result { + use crate::schema::users::dsl::*; + + let conn = &self + .0 + .get() + .map_err(|_| ServiceErrors::DatabaseConnectionLost)?; + let row: User = users + .distinct_on(id) + .filter(email.eq(msg.email.as_str())) + .filter(name.eq(msg.name.as_str())) + .first(conn) + .map_err(|_| ServiceErrors::RecordNotFound("project users".to_string()))?; + Ok(row) + } +} + #[derive(Serialize, Deserialize)] pub struct LoadProjectUsers { pub project_id: i32, diff --git a/jirs-server/src/ws/mod.rs b/jirs-server/src/ws/mod.rs index f2629741..9f7c23dd 100644 --- a/jirs-server/src/ws/mod.rs +++ b/jirs-server/src/ws/mod.rs @@ -83,6 +83,9 @@ impl WebSocketActor { // auth WsMsg::AuthorizeRequest(uuid) => block_on(self.authorize(uuid))?, + WsMsg::AuthenticateRequest(email, name) => { + block_on(users::authenticate(&self.db, name, email))? + } // users WsMsg::ProjectUsersRequest => { diff --git a/jirs-server/src/ws/users.rs b/jirs-server/src/ws/users.rs index 24089bc5..a9fab16f 100644 --- a/jirs-server/src/ws/users.rs +++ b/jirs-server/src/ws/users.rs @@ -3,10 +3,27 @@ use actix_web::web::Data; use jirs_data::WsMsg; -use crate::db::users::LoadProjectUsers; +use crate::db::users::{FindUser, 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,