Handle sign in process

This commit is contained in:
Adrian Woźniak 2020-04-16 16:11:19 +02:00
parent c0b5128777
commit 15095dc574
15 changed files with 160 additions and 61 deletions

2
Cargo.lock generated
View File

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

View File

@ -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<Msg>) {
}
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) => {

View File

@ -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<Msg>) {
pub fn update(msg: Msg, model: &mut model::Model, orders: &mut impl Orders<Msg>) {
if model.page != Page::Login {
return;
}
@ -33,15 +35,38 @@ pub fn update(msg: Msg, model: &mut model::Model, _orders: &mut impl Orders<Msg>
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<Msg> {
_ => 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<Msg> {
.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<Msg> {
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<Msg> {
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();

View File

@ -232,6 +232,7 @@ pub struct LoginPage {
pub email: String,
pub token: String,
pub login_success: bool,
pub bad_token: String,
}
#[derive(Debug)]

View File

@ -53,14 +53,6 @@ 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,

View File

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

View File

@ -474,6 +474,9 @@ pub enum WsMsg {
AuthorizeExpired,
AuthenticateRequest(EmailString, UsernameString),
AuthenticateSuccess,
BindTokenCheck(Uuid),
BindTokenBad,
BindTokenOk(Uuid),
// project page
ProjectRequest,

View File

@ -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" }

View File

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

View File

@ -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<FindBindToken> 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<Uuid> = 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<CreateBindToken> for DbExecutor {
type Result = Result<crate::models::Token, ServiceErrors>;
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)
}
}

View File

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

View File

@ -236,4 +236,5 @@ pub struct TokenForm {
pub user_id: i32,
pub access_token: Uuid,
pub refresh_token: Uuid,
pub bind_token: Option<Uuid>,
}

View File

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

View File

@ -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<Result<ws::Message, ws::ProtocolError>> for WebSocketActor {

View File

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