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 {
margin-right: 15px;
margin-right: 5px;
line-height: 32px;
}

View File

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

View File

@ -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<Msg>) {
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<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)
.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<Msg> {
.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<Msg> {
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();

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

View File

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

View File

@ -10,6 +10,7 @@ pub struct StyledInput {
icon: Option<Icon>,
valid: bool,
value: Option<String>,
input_type: Option<String>,
}
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<Icon>,
valid: Option<bool>,
value: Option<String>,
input_type: Option<String>,
}
impl StyledInputBuilder {
@ -50,12 +53,21 @@ impl StyledInputBuilder {
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 {
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<Msg> {
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<Msg> {
.dyn_ref::<web_sys::HtmlInputElement>()
.unwrap()
.value();
log!("asd");
Msg::InputChanged(field_id, value)
});
@ -109,6 +121,7 @@ pub fn render(values: StyledInput) -> Node<Msg> {
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,
],

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());
}
Msg::WsMsg(WsMsg::AuthorizeExpired) => {
use seed::*;
error!("Session expired");
if let Ok(msg) = write_auth_token(None) {
orders.skip().send_msg(msg);
}

View File

@ -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<User, String>),
AuthorizeExpired,
AuthenticateRequest(EmailString, UsernameString),
AuthenticateSuccess,
// project page
ProjectRequest,

View File

@ -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<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)]
pub struct LoadProjectUsers {
pub project_id: i32,

View File

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

View File

@ -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<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(
db: &Data<Addr<DbExecutor>>,
user: &Option<jirs_data::User>,