Add login page and start auth logic
This commit is contained in:
parent
5c3e8c8e93
commit
62b6c77666
@ -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;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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),
|
||||||
|
|
||||||
|
@ -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();
|
||||||
|
|
||||||
|
@ -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),
|
||||||
}
|
}
|
||||||
|
@ -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)
|
||||||
|
@ -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,
|
||||||
],
|
],
|
||||||
|
@ -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);
|
||||||
}
|
}
|
||||||
|
@ -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,
|
||||||
|
@ -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,
|
||||||
|
@ -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 => {
|
||||||
|
@ -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>,
|
||||||
|
Loading…
Reference in New Issue
Block a user