Add login page and start auth logic

This commit is contained in:
Adrian Wozniak 2020-04-15 20:28:07 +02:00
parent 5c3e8c8e93
commit 62b6c77666
11 changed files with 162 additions and 15 deletions

View File

@ -28,7 +28,7 @@
} }
#login > .styledForm > .formElement > .noPasswordSection > .styledIcon { #login > .styledForm > .formElement > .noPasswordSection > .styledIcon {
margin-right: 15px; margin-right: 5px;
line-height: 32px; line-height: 32px;
} }

View File

@ -6,7 +6,6 @@ use jirs_data::*;
use crate::api::send_ws_msg; use crate::api::send_ws_msg;
use crate::model::{ModalType, Model, Page}; use crate::model::{ModalType, Model, Page};
use crate::shared::read_auth_token;
use crate::shared::styled_editor::Mode as TabMode; use crate::shared::styled_editor::Mode as TabMode;
use crate::shared::styled_select::StyledSelectChange; use crate::shared::styled_select::StyledSelectChange;
@ -59,7 +58,9 @@ pub enum ProjectSettingsFieldId {
#[derive(Clone, Debug, PartialOrd, PartialEq, Hash)] #[derive(Clone, Debug, PartialOrd, PartialEq, Hash)]
pub enum LoginFieldId { pub enum LoginFieldId {
Username,
Email, Email,
Token,
} }
#[derive(Clone, Debug, PartialOrd, PartialEq, Hash)] #[derive(Clone, Debug, PartialOrd, PartialEq, Hash)]
@ -109,6 +110,8 @@ impl std::fmt::Display for FieldId {
}, },
FieldId::Login(sub) => match sub { FieldId::Login(sub) => match sub {
LoginFieldId::Email => f.write_str("login-email"), 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 // Auth Token
AuthTokenStored, AuthTokenStored,
AuthTokenErased, AuthTokenErased,
SignInRequest,
StyledSelectChanged(FieldId, StyledSelectChange), StyledSelectChanged(FieldId, StyledSelectChange),

View File

@ -1,36 +1,72 @@
use seed::{prelude::*, *}; 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_field::StyledField;
use crate::shared::styled_form::StyledForm; use crate::shared::styled_form::StyledForm;
use crate::shared::styled_icon::{Icon, StyledIcon}; use crate::shared::styled_icon::{Icon, StyledIcon};
use crate::shared::styled_input::StyledInput; use crate::shared::styled_input::StyledInput;
use crate::shared::{outer_layout, ToNode}; use crate::shared::{outer_layout, ToNode};
use crate::{model, FieldId, LoginFieldId, Msg}; use crate::{model, FieldId, LoginFieldId, Msg};
use jirs_data::WsMsg;
pub fn update(msg: Msg, model: &mut model::Model, _orders: &mut impl Orders<Msg>) { pub fn update(msg: Msg, model: &mut model::Model, _orders: &mut impl Orders<Msg>) {
if model.page != Page::Login { if model.page != Page::Login {
return; 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 { 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<Msg> { pub fn view(model: &model::Model) -> Node<Msg> {
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) .valid(true)
.value(page.username.as_str())
.build() .build()
.into_node(); .into_node();
let login_field = StyledField::build() let username_field = StyledField::build()
.label("Login") .label("Username")
.input(login) .input(username)
.build() .build()
.into_node(); .into_node();
let email = StyledInput::build(FieldId::Login(LoginFieldId::Email)) let email = StyledInput::build(FieldId::Login(LoginFieldId::Email))
.valid(true) .valid(true)
.value(page.email.as_str())
.input_type("email")
.build() .build()
.into_node(); .into_node();
let email_field = StyledField::build() let email_field = StyledField::build()
@ -39,6 +75,18 @@ pub fn view(model: &model::Model) -> Node<Msg> {
.build() .build()
.into_node(); .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) let help_icon = StyledIcon::build(Icon::Help)
.add_class("noPasswordHelp") .add_class("noPasswordHelp")
.size(22) .size(22)
@ -52,11 +100,32 @@ pub fn view(model: &model::Model) -> Node<Msg> {
span!["Why I don't see password?"] 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() let form = StyledForm::build()
.heading("Sign In to your account") .heading("Sign In to your account")
.add_field(login_field) .add_field(username_field)
.add_field(email_field) .add_field(email_field)
.add_field(submit_field)
.add_field(no_pass_section) .add_field(no_pass_section)
.add_field(token_field)
.add_field(submit_token_field)
.build() .build()
.into_node(); .into_node();

View File

@ -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)] #[derive(Debug)]
pub enum PageContent { pub enum PageContent {
Login(LoginPage),
Project(ProjectPage), Project(ProjectPage),
ProjectSettings(ProjectSettingsPage), ProjectSettings(ProjectSettingsPage),
} }

View File

@ -47,9 +47,9 @@ impl StyledButtonBuilder {
self.variant(Variant::Primary) self.variant(Variant::Primary)
} }
// pub fn success(self) -> Self { pub fn success(self) -> Self {
// self.variant(Variant::Success) self.variant(Variant::Success)
// } }
// pub fn danger(self) -> Self { // pub fn danger(self) -> Self {
// self.variant(Variant::Danger) // self.variant(Variant::Danger)

View File

@ -10,6 +10,7 @@ pub struct StyledInput {
icon: Option<Icon>, icon: Option<Icon>,
valid: bool, valid: bool,
value: Option<String>, value: Option<String>,
input_type: Option<String>,
} }
impl StyledInput { impl StyledInput {
@ -19,6 +20,7 @@ impl StyledInput {
icon: None, icon: None,
valid: None, valid: None,
value: None, value: None,
input_type: None,
} }
} }
} }
@ -29,6 +31,7 @@ pub struct StyledInputBuilder {
icon: Option<Icon>, icon: Option<Icon>,
valid: Option<bool>, valid: Option<bool>,
value: Option<String>, value: Option<String>,
input_type: Option<String>,
} }
impl StyledInputBuilder { impl StyledInputBuilder {
@ -50,12 +53,21 @@ impl StyledInputBuilder {
self self
} }
pub fn input_type<S>(mut self, input_type: S) -> Self
where
S: Into<String>,
{
self.input_type = Some(input_type.into());
self
}
pub fn build(self) -> StyledInput { pub fn build(self) -> StyledInput {
StyledInput { StyledInput {
id: self.id, id: self.id,
icon: self.icon, icon: self.icon,
valid: self.valid.unwrap_or_default(), valid: self.valid.unwrap_or_default(),
value: self.value, value: self.value,
input_type: self.input_type,
} }
} }
} }
@ -72,6 +84,7 @@ pub fn render(values: StyledInput) -> Node<Msg> {
icon, icon,
valid, valid,
value, value,
input_type,
} = values; } = values;
let mut wrapper_class_list = vec!["styledInput".to_string(), format!("{}", id)]; let mut wrapper_class_list = vec!["styledInput".to_string(), format!("{}", id)];
@ -98,7 +111,6 @@ pub fn render(values: StyledInput) -> Node<Msg> {
.dyn_ref::<web_sys::HtmlInputElement>() .dyn_ref::<web_sys::HtmlInputElement>()
.unwrap() .unwrap()
.value(); .value();
log!("asd");
Msg::InputChanged(field_id, value) Msg::InputChanged(field_id, value)
}); });
@ -109,6 +121,7 @@ pub fn render(values: StyledInput) -> Node<Msg> {
attrs![ attrs![
At::Class => input_class_list.join(" "), At::Class => input_class_list.join(" "),
At::Value => value.unwrap_or_default(), At::Value => value.unwrap_or_default(),
At::Type => input_type.unwrap_or_else(|| "text".to_string()),
], ],
change_handler, change_handler,
], ],

View File

@ -26,8 +26,6 @@ pub fn update(msg: &Msg, model: &mut model::Model, orders: &mut impl Orders<Msg>
model.user = Some(user.clone()); model.user = Some(user.clone());
} }
Msg::WsMsg(WsMsg::AuthorizeExpired) => { Msg::WsMsg(WsMsg::AuthorizeExpired) => {
use seed::*;
error!("Session expired");
if let Ok(msg) = write_auth_token(None) { if let Ok(msg) = write_auth_token(None) {
orders.skip().send_msg(msg); orders.skip().send_msg(msg);
} }

View File

@ -22,6 +22,8 @@ pub type ProjectId = i32;
pub type UserId = i32; pub type UserId = i32;
pub type CommentId = i32; pub type CommentId = i32;
pub type TokenId = i32; pub type TokenId = i32;
pub type EmailString = String;
pub type UsernameString = String;
#[cfg_attr(feature = "backend", derive(FromSqlRow, AsExpression))] #[cfg_attr(feature = "backend", derive(FromSqlRow, AsExpression))]
#[cfg_attr(feature = "backend", sql_type = "IssueTypeType")] #[cfg_attr(feature = "backend", sql_type = "IssueTypeType")]
@ -470,6 +472,8 @@ pub enum WsMsg {
AuthorizeRequest(Uuid), AuthorizeRequest(Uuid),
AuthorizeLoaded(Result<User, String>), AuthorizeLoaded(Result<User, String>),
AuthorizeExpired, AuthorizeExpired,
AuthenticateRequest(EmailString, UsernameString),
AuthenticateSuccess,
// project page // project page
ProjectRequest, ProjectRequest,

View File

@ -5,6 +5,36 @@ use actix::{Handler, Message};
use diesel::prelude::*; use diesel::prelude::*;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
#[derive(Serialize, Deserialize)]
pub struct FindUser {
pub name: String,
pub email: String,
}
impl Message for FindUser {
type Result = Result<User, ServiceErrors>;
}
impl Handler<FindUser> for DbExecutor {
type Result = Result<User, ServiceErrors>;
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)] #[derive(Serialize, Deserialize)]
pub struct LoadProjectUsers { pub struct LoadProjectUsers {
pub project_id: i32, pub project_id: i32,

View File

@ -83,6 +83,9 @@ impl WebSocketActor {
// auth // auth
WsMsg::AuthorizeRequest(uuid) => block_on(self.authorize(uuid))?, WsMsg::AuthorizeRequest(uuid) => block_on(self.authorize(uuid))?,
WsMsg::AuthenticateRequest(email, name) => {
block_on(users::authenticate(&self.db, name, email))?
}
// users // users
WsMsg::ProjectUsersRequest => { WsMsg::ProjectUsersRequest => {

View File

@ -3,10 +3,27 @@ use actix_web::web::Data;
use jirs_data::WsMsg; use jirs_data::WsMsg;
use crate::db::users::LoadProjectUsers; use crate::db::users::{FindUser, LoadProjectUsers};
use crate::db::DbExecutor; use crate::db::DbExecutor;
use crate::ws::{current_user, WsResult}; use crate::ws::{current_user, WsResult};
pub async fn authenticate(db: &Data<Addr<DbExecutor>>, 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( pub async fn load_project_users(
db: &Data<Addr<DbExecutor>>, db: &Data<Addr<DbExecutor>>,
user: &Option<jirs_data::User>, user: &Option<jirs_data::User>,