From 372b9d9b8d566a8a28418af6a371ef6a8aed0ed7 Mon Sep 17 00:00:00 2001 From: Adrian Wozniak Date: Thu, 21 May 2020 17:02:16 +0200 Subject: [PATCH] Add multi project support. --- .cargo/config | 9 ++ jirs-client/src/invite.rs | 6 +- jirs-client/src/lib.rs | 1 + jirs-client/src/model.rs | 26 +++- jirs-client/src/profile.rs | 20 ++- jirs-client/src/project.rs | 30 ++-- jirs-client/src/project_settings.rs | 32 ++-- jirs-client/src/shared/aside.rs | 20 ++- jirs-client/src/sign_in.rs | 8 +- jirs-client/src/sign_up.rs | 8 +- jirs-client/src/users.rs | 3 +- jirs-client/src/ws/mod.rs | 27 ++++ jirs-data/src/lib.rs | 27 +++- jirs-server/Cargo.toml | 10 -- .../down.sql | 22 +++ .../up.sql | 22 +++ .../down.sql | 1 + .../2020-05-21-064702_create_messages/up.sql | 11 ++ jirs-server/seed.sql | 14 +- jirs-server/src/db/invitations.rs | 32 ++-- jirs-server/src/db/mod.rs | 1 + jirs-server/src/db/projects.rs | 34 ++++- jirs-server/src/db/user_projects.rs | 111 ++++++++++++++ jirs-server/src/db/users.rs | 76 +++++++--- jirs-server/src/models.rs | 1 - jirs-server/src/schema.rs | 141 ++++++++++++++++-- jirs-server/src/web/avatar.rs | 16 +- jirs-server/src/ws/auth.rs | 3 + jirs-server/src/ws/comments.rs | 36 ++++- jirs-server/src/ws/invitations.rs | 49 ++++-- jirs-server/src/ws/issue_statuses.rs | 44 +++++- jirs-server/src/ws/issues.rs | 29 +++- jirs-server/src/ws/mod.rs | 64 ++++++-- jirs-server/src/ws/projects.rs | 4 +- jirs-server/src/ws/user_projects.rs | 52 +++++++ jirs-server/src/ws/users.rs | 25 +++- 36 files changed, 859 insertions(+), 156 deletions(-) create mode 100644 .cargo/config create mode 100644 jirs-server/migrations/2020-05-21-051229_multi_project_users/down.sql create mode 100644 jirs-server/migrations/2020-05-21-051229_multi_project_users/up.sql create mode 100644 jirs-server/migrations/2020-05-21-064702_create_messages/down.sql create mode 100644 jirs-server/migrations/2020-05-21-064702_create_messages/up.sql create mode 100644 jirs-server/src/db/user_projects.rs create mode 100644 jirs-server/src/ws/user_projects.rs diff --git a/.cargo/config b/.cargo/config new file mode 100644 index 00000000..b3678508 --- /dev/null +++ b/.cargo/config @@ -0,0 +1,9 @@ +[target.x86_64-unknown-linux-gnu] +rustflags = [ + "-C", "link-arg=-fuse-ld=lld", +] + +[target.nightly-x86_64-unknown-linux-gnu] +rustflags = [ + "-C", "link-arg=-fuse-ld=lld", +] diff --git a/jirs-client/src/invite.rs b/jirs-client/src/invite.rs index 592719a9..7b8008d3 100644 --- a/jirs-client/src/invite.rs +++ b/jirs-client/src/invite.rs @@ -12,7 +12,7 @@ use crate::{FieldId, Msg}; pub fn update(msg: Msg, model: &mut Model, _orders: &mut impl Orders) { if let Msg::ChangePage(Page::Project) = msg { - model.page_content = PageContent::Invite(Box::new(InvitePage::default())); + build_page_content(model); return; } @@ -27,6 +27,10 @@ pub fn update(msg: Msg, model: &mut Model, _orders: &mut impl Orders) { } } +fn build_page_content(model: &mut Model) { + model.page_content = PageContent::Invite(Box::new(InvitePage::default())); +} + pub fn view(model: &Model) -> Node { let page = match &model.page_content { PageContent::Invite(page) => page, diff --git a/jirs-client/src/lib.rs b/jirs-client/src/lib.rs index c900e8fa..cd32cb54 100644 --- a/jirs-client/src/lib.rs +++ b/jirs-client/src/lib.rs @@ -175,6 +175,7 @@ fn update(msg: Msg, model: &mut model::Model, orders: &mut impl Orders) { _ => (), } crate::modal::update(&msg, model, orders); + crate::shared::aside::update(&msg, model, orders); match model.page { Page::Project | Page::AddIssue | Page::EditIssue(..) => project::update(msg, model, orders), Page::ProjectSettings => project_settings::update(msg, model, orders), diff --git a/jirs-client/src/model.rs b/jirs-client/src/model.rs index 4bf00265..fdae99c6 100644 --- a/jirs-client/src/model.rs +++ b/jirs-client/src/model.rs @@ -458,11 +458,14 @@ pub struct Model { pub project: Option, pub user: Option, + pub current_user_project: Option, pub issues: Vec, pub users: Vec, pub comments: Vec, pub issue_statuses: Vec, pub messages: Vec, + pub user_projects: Vec, + pub projects: Vec, } impl Model { @@ -475,8 +478,6 @@ impl Model { issue_form: None, project_form: None, comment_form: None, - issues: vec![], - users: vec![], comments_by_project_id: Default::default(), page: Page::Project, host_url, @@ -484,9 +485,12 @@ impl Model { page_content: PageContent::Project(Box::new(ProjectPage::default())), modals: vec![], project: None, - comments: vec![], + current_user_project: None, about_tooltip_visible: false, messages_tooltip_visible: false, + issues: vec![], + users: vec![], + comments: vec![], issue_statuses: vec![], messages: vec![Message { id: 0, @@ -499,6 +503,22 @@ impl Model { created_at: chrono::NaiveDateTime::from_timestamp(4567890, 123), updated_at: chrono::NaiveDateTime::from_timestamp(1234567, 098), }], + user_projects: vec![], + projects: vec![], } } + + pub fn current_user_role(&self) -> UserRole { + self.current_user_project + .as_ref() + .map(|up| up.role) + .unwrap_or_default() + } + + pub fn current_project_id(&self) -> ProjectId { + self.current_user_project + .as_ref() + .map(|up| up.project_id) + .unwrap_or_default() + } } diff --git a/jirs-client/src/profile.rs b/jirs-client/src/profile.rs index 33e59e15..b7285563 100644 --- a/jirs-client/src/profile.rs +++ b/jirs-client/src/profile.rs @@ -17,12 +17,8 @@ pub fn update(msg: Msg, model: &mut crate::model::Model, orders: &mut impl Order match msg { Msg::WebSocketChange(WebSocketChanged::WsMsg(WsMsg::AuthorizeLoaded(..))) | Msg::ChangePage(Page::Profile) => { - send_ws_msg(WsMsg::ProjectRequest, model.ws.as_ref(), orders); - let user = match model.user { - Some(ref user) => user, - _ => return, - }; - model.page_content = PageContent::Profile(Box::new(ProfilePage::new(user))); + init_load(model, orders); + build_page_content(model); } _ => (), } @@ -77,6 +73,18 @@ pub fn update(msg: Msg, model: &mut crate::model::Model, orders: &mut impl Order } } +fn init_load(model: &mut Model, orders: &mut impl Orders) { + send_ws_msg(WsMsg::ProjectRequest, model.ws.as_ref(), orders); +} + +fn build_page_content(model: &mut Model) { + let user = match model.user { + Some(ref user) => user, + _ => return, + }; + model.page_content = PageContent::Profile(Box::new(ProfilePage::new(user))); +} + pub fn view(model: &Model) -> Node { let page = match &model.page_content { PageContent::Profile(profile_page) => profile_page, diff --git a/jirs-client/src/project.rs b/jirs-client/src/project.rs index ad5b3cb4..62f710b7 100644 --- a/jirs-client/src/project.rs +++ b/jirs-client/src/project.rs @@ -22,7 +22,7 @@ pub fn update(msg: Msg, model: &mut crate::model::Model, orders: &mut impl Order Msg::ChangePage(Page::Project) | Msg::ChangePage(Page::AddIssue) | Msg::ChangePage(Page::EditIssue(..)) => { - model.page_content = PageContent::Project(Box::new(ProjectPage::default())); + build_page_content(model); } _ => (), } @@ -37,16 +37,7 @@ pub fn update(msg: Msg, model: &mut crate::model::Model, orders: &mut impl Order | Msg::ChangePage(Page::Project) | Msg::ChangePage(Page::AddIssue) | Msg::ChangePage(Page::EditIssue(..)) => { - enqueue_ws_msg( - vec![ - jirs_data::WsMsg::ProjectRequest, - jirs_data::WsMsg::ProjectIssuesRequest, - jirs_data::WsMsg::ProjectUsersRequest, - jirs_data::WsMsg::IssueStatusesRequest, - ], - model.ws.as_ref(), - orders, - ); + init_load(model, orders); } Msg::WebSocketChange(WebSocketChanged::WsMsg(WsMsg::IssueUpdated(issue))) => { let mut old: Vec = vec![]; @@ -139,6 +130,23 @@ pub fn update(msg: Msg, model: &mut crate::model::Model, orders: &mut impl Order } } +fn init_load(model: &mut Model, orders: &mut impl Orders) { + enqueue_ws_msg( + vec![ + jirs_data::WsMsg::ProjectRequest, + jirs_data::WsMsg::ProjectIssuesRequest, + jirs_data::WsMsg::ProjectUsersRequest, + jirs_data::WsMsg::IssueStatusesRequest, + ], + model.ws.as_ref(), + orders, + ); +} + +fn build_page_content(model: &mut Model) { + model.page_content = PageContent::Project(Box::new(ProjectPage::default())); +} + pub fn view(model: &Model) -> Node { let project_section = vec![ breadcrumbs(model), diff --git a/jirs-client/src/project_settings.rs b/jirs-client/src/project_settings.rs index 563f5708..213804ec 100644 --- a/jirs-client/src/project_settings.rs +++ b/jirs-client/src/project_settings.rs @@ -391,15 +391,7 @@ pub fn update(msg: Msg, model: &mut model::Model, orders: &mut impl Orders) match msg { Msg::WebSocketChange(ref change) => match change { WebSocketChanged::WsMsg(WsMsg::AuthorizeLoaded(..)) => { - enqueue_ws_msg( - vec![ - WsMsg::ProjectRequest, - WsMsg::IssueStatusesRequest, - WsMsg::ProjectIssuesRequest, - ], - model.ws.as_ref(), - orders, - ); + init_load(model, orders); } WebSocketChanged::WsMsg(WsMsg::ProjectLoaded(..)) => { build_page_content(model); @@ -417,15 +409,7 @@ pub fn update(msg: Msg, model: &mut model::Model, orders: &mut impl Orders) Msg::ChangePage(Page::ProjectSettings) => { build_page_content(model); if model.user.is_some() { - enqueue_ws_msg( - vec![ - WsMsg::ProjectRequest, - WsMsg::IssueStatusesRequest, - WsMsg::ProjectIssuesRequest, - ], - model.ws.as_ref(), - orders, - ); + init_load(model, orders); } } _ => (), @@ -547,6 +531,18 @@ pub fn update(msg: Msg, model: &mut model::Model, orders: &mut impl Orders) } } +fn init_load(model: &mut Model, orders: &mut impl Orders) { + enqueue_ws_msg( + vec![ + WsMsg::ProjectRequest, + WsMsg::IssueStatusesRequest, + WsMsg::ProjectIssuesRequest, + ], + model.ws.as_ref(), + orders, + ); +} + fn exchange_position(bellow_id: IssueStatusId, model: &mut Model) { let page = match &mut model.page_content { PageContent::ProjectSettings(page) => page, diff --git a/jirs-client/src/shared/aside.rs b/jirs-client/src/shared/aside.rs index aa453279..89c75d49 100644 --- a/jirs-client/src/shared/aside.rs +++ b/jirs-client/src/shared/aside.rs @@ -1,11 +1,25 @@ use seed::{prelude::*, *}; -use jirs_data::UserRole; +use jirs_data::{UserRole, WsMsg}; use crate::model::{Model, Page}; use crate::shared::styled_icon::{Icon, StyledIcon}; use crate::shared::{divider, ToNode}; -use crate::Msg; +use crate::ws::enqueue_ws_msg; +use crate::{Msg, WebSocketChanged}; + +pub fn update(msg: &Msg, model: &mut Model, orders: &mut impl Orders) { + match msg { + Msg::WebSocketChange(WebSocketChanged::WsMsg(WsMsg::AuthorizeLoaded(Ok(_)))) => { + enqueue_ws_msg( + vec![WsMsg::UserProjectLoad, WsMsg::ProjectsLoad], + model.ws.as_ref(), + orders, + ); + } + _ => (), + } +} pub fn render(model: &Model) -> Node { let project_icon = Node::from_html(include_str!("../../static/project-avatar.svg")); @@ -36,7 +50,7 @@ pub fn render(model: &Model) -> Node { sidebar_link_item(model, "Components", Icon::Component, None), ]; - if model.user.as_ref().map(|u| u.user_role).unwrap_or_default() > UserRole::User { + if model.current_user_role() > UserRole::User { links.push(sidebar_link_item( model, "Users", diff --git a/jirs-client/src/sign_in.rs b/jirs-client/src/sign_in.rs index 5194df10..5364af3e 100644 --- a/jirs-client/src/sign_in.rs +++ b/jirs-client/src/sign_in.rs @@ -5,7 +5,7 @@ use uuid::Uuid; use jirs_data::WsMsg; -use crate::model::{Page, PageContent, SignInPage}; +use crate::model::{Model, Page, PageContent, SignInPage}; use crate::shared::styled_button::StyledButton; use crate::shared::styled_field::StyledField; use crate::shared::styled_form::StyledForm; @@ -24,7 +24,7 @@ pub fn update(msg: Msg, model: &mut model::Model, orders: &mut impl Orders) match msg { Msg::ChangePage(Page::SignIn) => { - model.page_content = PageContent::SignIn(Box::new(SignInPage::default())); + build_page_content(model); return; } _ => (), @@ -85,6 +85,10 @@ pub fn update(msg: Msg, model: &mut model::Model, orders: &mut impl Orders) }; } +fn build_page_content(model: &mut Model) { + model.page_content = PageContent::SignIn(Box::new(SignInPage::default())); +} + pub fn view(model: &model::Model) -> Node { let page = match &model.page_content { PageContent::SignIn(page) => page, diff --git a/jirs-client/src/sign_up.rs b/jirs-client/src/sign_up.rs index bb21d82e..08e4e9c8 100644 --- a/jirs-client/src/sign_up.rs +++ b/jirs-client/src/sign_up.rs @@ -2,7 +2,7 @@ use seed::{prelude::*, *}; use jirs_data::{SignUpFieldId, WsMsg}; -use crate::model::{Page, PageContent, SignUpPage}; +use crate::model::{Model, Page, PageContent, SignUpPage}; use crate::shared::styled_button::StyledButton; use crate::shared::styled_field::StyledField; use crate::shared::styled_form::StyledForm; @@ -21,7 +21,7 @@ pub fn update(msg: Msg, model: &mut model::Model, orders: &mut impl Orders) match msg { Msg::ChangePage(Page::SignUp) => { - model.page_content = PageContent::SignUp(Box::new(SignUpPage::default())); + build_page_content(model); return; } _ => (), @@ -61,6 +61,10 @@ pub fn update(msg: Msg, model: &mut model::Model, orders: &mut impl Orders) } } +fn build_page_content(model: &mut Model) { + model.page_content = PageContent::SignUp(Box::new(SignUpPage::default())); +} + pub fn view(model: &model::Model) -> Node { let page = match &model.page_content { PageContent::SignUp(page) => page, diff --git a/jirs-client/src/users.rs b/jirs-client/src/users.rs index 0c9d79bc..7e6cdf63 100644 --- a/jirs-client/src/users.rs +++ b/jirs-client/src/users.rs @@ -231,11 +231,12 @@ pub fn view(model: &Model) -> Node { .on_click(mouse_ev(Ev::Click, move |_| Msg::InvitedUserRemove(email))) .build() .into_node(); + + // span![format!("{}", user.user_role)], li![ class!["user"], span![user.name.as_str()], span![user.email.as_str()], - span![format!("{}", user.user_role)], remove, ] }) diff --git a/jirs-client/src/ws/mod.rs b/jirs-client/src/ws/mod.rs index 3cacf2b6..13b70710 100644 --- a/jirs-client/src/ws/mod.rs +++ b/jirs-client/src/ws/mod.rs @@ -90,6 +90,33 @@ pub fn update(msg: &WsMsg, model: &mut Model, orders: &mut impl Orders) { WsMsg::ProjectLoaded(project) => { model.project = Some(project.clone()); } + WsMsg::ProjectsLoaded(v) => { + model.projects = v.clone(); + if !model.projects.is_empty() { + model.project = model.current_user_project.as_ref().and_then(|up| { + model + .projects + .iter() + .find(|p| p.id == up.project_id) + .cloned() + }); + } + } + // user projects + WsMsg::UserProjectLoaded(v) => { + model.user_projects = v.clone(); + model.current_user_project = v.iter().find(|up| up.is_current).cloned(); + if !model.projects.is_empty() { + model.project = model.current_user_project.as_ref().and_then(|up| { + model + .projects + .iter() + .find(|p| p.id == up.project_id) + .cloned() + }); + } + } + // issues WsMsg::ProjectIssuesLoaded(v) => { let mut v = v.clone(); diff --git a/jirs-data/src/lib.rs b/jirs-data/src/lib.rs index d82c243d..9da11c1a 100644 --- a/jirs-data/src/lib.rs +++ b/jirs-data/src/lib.rs @@ -21,6 +21,7 @@ pub trait ToVec { pub type IssueId = i32; pub type ProjectId = i32; pub type UserId = i32; +pub type UserProjectId = i32; pub type CommentId = i32; pub type TokenId = i32; pub type IssueStatusId = i32; @@ -468,10 +469,21 @@ pub struct User { pub name: String, pub email: String, pub avatar_url: Option, - pub project_id: ProjectId, pub created_at: NaiveDateTime, pub updated_at: NaiveDateTime, - pub user_role: UserRole, +} + +#[cfg_attr(feature = "backend", derive(Queryable))] +#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)] +pub struct UserProject { + pub id: UserProjectId, + pub user_id: UserId, + pub project_id: ProjectId, + pub is_default: bool, + pub is_current: bool, + pub role: UserRole, + pub created_at: NaiveDateTime, + pub updated_at: NaiveDateTime, } #[cfg_attr(feature = "backend", derive(Queryable))] @@ -533,6 +545,7 @@ impl From for UpdateIssuePayload { } } +#[cfg_attr(feature = "backend", derive(Queryable))] #[derive(Clone, Debug, Serialize, Deserialize, PartialEq)] pub struct Message { pub id: MessageId, @@ -656,6 +669,7 @@ pub enum IssueFieldId { pub enum WsMsg { Ping, Pong, + Die, // auth AuthorizeRequest(Uuid), @@ -697,6 +711,9 @@ pub enum WsMsg { // project page ProjectRequest, ProjectLoaded(Project), + ProjectsLoad, + ProjectsLoaded(Vec), + ProjectIssuesRequest, ProjectIssuesLoaded(Vec), ProjectUsersRequest, @@ -734,6 +751,12 @@ pub enum WsMsg { ProfileUpdate(EmailString, UsernameString), ProfileUpdated, + // user projects + UserProjectLoad, + UserProjectLoaded(Vec), + UserProjectSetCurrent(UserProjectId), + UserProjectCurrentChanged(UserProject), + // messages Message(Message), MessagesRequest, diff --git a/jirs-server/Cargo.toml b/jirs-server/Cargo.toml index a7f2237b..a7488134 100644 --- a/jirs-server/Cargo.toml +++ b/jirs-server/Cargo.toml @@ -8,16 +8,6 @@ repository = "https://gitlab.com/adrian.wozniak/jirs" license = "MPL-2.0" #license-file = "../LICENSE" -[target.x86_64-unknown-linux-gnu] -rustflags = [ - "-C", "link-arg=-fuse-ld=lld", -] - -[target.nightly-x86_64-unknown-linux-gnu] -rustflags = [ - "-C", "link-arg=-fuse-ld=lld", -] - [[bin]] name = "jirs_server" path = "./src/main.rs" diff --git a/jirs-server/migrations/2020-05-21-051229_multi_project_users/down.sql b/jirs-server/migrations/2020-05-21-051229_multi_project_users/down.sql new file mode 100644 index 00000000..3c185747 --- /dev/null +++ b/jirs-server/migrations/2020-05-21-051229_multi_project_users/down.sql @@ -0,0 +1,22 @@ +BEGIN; + +ALTER TABLE users ADD COLUMN role "UserRoleType" DEFAULT 'user' NOT NULL; +ALTER TABLE users ADD COLUMN project_id int; + +UPDATE users +SET project_id = user_projects.project_id, + role = user_projects.role +FROM user_projects +INNER JOIN user_projects +ON user_projects.user_id = users.id; + +DROP TABLE user_projects; + +ALTER TABLE users +ALTER COLUMN project_id +ADD CONSTRAINT users_project_id_fkey +FOREIGN KEY (project_id) +REFERENCES projects (id) +MATCH FULL; + +COMMIT; diff --git a/jirs-server/migrations/2020-05-21-051229_multi_project_users/up.sql b/jirs-server/migrations/2020-05-21-051229_multi_project_users/up.sql new file mode 100644 index 00000000..508c807e --- /dev/null +++ b/jirs-server/migrations/2020-05-21-051229_multi_project_users/up.sql @@ -0,0 +1,22 @@ +BEGIN; + +DROP TABLE IF EXISTS user_projects CASCADE; +CREATE TABLE user_projects ( + id serial primary key not null, + user_id int not null references users (id), + project_id int not null references projects (id), + is_default bool not null default false, + is_current bool not null default false, + role "UserRoleType" not null default 'user', + created_at timestamp not null default now(), + updated_at timestamp not null default now() +); + +INSERT INTO user_projects (user_id, project_id, role, is_default, is_current) +SELECT id, project_id, role, true, true +FROM users; + +ALTER TABLE users DROP COLUMN role; +ALTER TABLE users DROP COLUMN project_id; + +COMMIT; diff --git a/jirs-server/migrations/2020-05-21-064702_create_messages/down.sql b/jirs-server/migrations/2020-05-21-064702_create_messages/down.sql new file mode 100644 index 00000000..cbe81899 --- /dev/null +++ b/jirs-server/migrations/2020-05-21-064702_create_messages/down.sql @@ -0,0 +1 @@ +DROP TABLE IF EXISTS messages; diff --git a/jirs-server/migrations/2020-05-21-064702_create_messages/up.sql b/jirs-server/migrations/2020-05-21-064702_create_messages/up.sql new file mode 100644 index 00000000..7e75de34 --- /dev/null +++ b/jirs-server/migrations/2020-05-21-064702_create_messages/up.sql @@ -0,0 +1,11 @@ +CREATE TABLE messages ( + id serial primary key not null, + receiver_id int not null references users (id), + sender_id int not null references users (id), + summary text not null, + description text not null, + message_type text not null, + hyper_link text not null, + created_at timestamp not null default now(), + updated_at timestamp not null default now() +); diff --git a/jirs-server/seed.sql b/jirs-server/seed.sql index c0a1c969..d3a85592 100644 --- a/jirs-server/seed.sql +++ b/jirs-server/seed.sql @@ -3,23 +3,27 @@ insert into projects (name) values ('initial'), ('second'), ('third'); insert into issue_statuses (name, project_id, position) values ('backlog', 1, 1), ('selected', 1, 2), ('in_progress', 1, 3), ('done', 1, 4); -insert into users (project_id, email, name, avatar_url) values ( - 1, +insert into users (email, name, avatar_url) values ( '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, 'mike@example.com', 'Mike Keningham', 'https://cdn0.iconfinder.com/data/icons/user-pictures/100/matureman1-512.png' ); -insert into invitations ( email, name, state, project_id, invited_by_id) values ( +insert into user_projects (user_id, project_id, role, is_current, is_default) values ( + 1, 1, 'owner', true, true +), ( + 2, 1, 'owner', true, true +), ( + 3, 1, 'owner', true, true +); +insert into invitations (email, name, state, project_id, invited_by_id) values ( 'foo1@example.com', 'Foo1', 'sent', diff --git a/jirs-server/src/db/invitations.rs b/jirs-server/src/db/invitations.rs index 0e422be6..5bd6240f 100644 --- a/jirs-server/src/db/invitations.rs +++ b/jirs-server/src/db/invitations.rs @@ -8,7 +8,7 @@ use jirs_data::{ use crate::db::DbExecutor; use crate::errors::ServiceErrors; -use crate::models::{InvitationForm, UserForm}; +use crate::models::InvitationForm; pub struct ListInvitation { pub user_id: UserId, @@ -150,7 +150,6 @@ impl Handler for DbExecutor { fn handle(&mut self, msg: AcceptInvitation, _ctx: &mut Self::Context) -> Self::Result { use crate::schema::invitations::dsl::*; - use crate::schema::users::dsl::users; let conn = &self .pool @@ -181,17 +180,26 @@ impl Handler for DbExecutor { .execute(conn) .map_err(|e| ServiceErrors::DatabaseQueryFailed(format!("{}", e)))?; - let form = UserForm { - name: invitation.name, - email: invitation.email, - avatar_url: None, - project_id: invitation.project_id, + let user: User = { + use crate::schema::users::dsl::*; + + let query = diesel::insert_into(users) + .values((name.eq(invitation.name), email.eq(invitation.email))); + debug!("{}", diesel::debug_query::(&query)); + query + .get_result(conn) + .map_err(|e| ServiceErrors::DatabaseQueryFailed(format!("{}", e)))? + }; + { + use crate::schema::user_projects::dsl::*; + + let query = diesel::insert_into(user_projects) + .values((user_id.eq(user.id), project_id.eq(invitation.project_id))); + debug!("{}", diesel::debug_query::(&query)); + query + .execute(conn) + .map_err(|e| ServiceErrors::DatabaseQueryFailed(format!("{}", e)))?; }; - let query = diesel::insert_into(users).values(form); - debug!("{}", diesel::debug_query::(&query).to_string()); - let user: User = query - .get_result(conn) - .map_err(|e| ServiceErrors::DatabaseQueryFailed(format!("{}", e)))?; Ok(user) } diff --git a/jirs-server/src/db/mod.rs b/jirs-server/src/db/mod.rs index 88fd9293..80c98d09 100644 --- a/jirs-server/src/db/mod.rs +++ b/jirs-server/src/db/mod.rs @@ -17,6 +17,7 @@ pub mod issue_statuses; pub mod issues; pub mod projects; pub mod tokens; +pub mod user_projects; pub mod users; #[cfg(debug_assertions)] diff --git a/jirs-server/src/db/projects.rs b/jirs-server/src/db/projects.rs index 1078ceef..36f5892d 100644 --- a/jirs-server/src/db/projects.rs +++ b/jirs-server/src/db/projects.rs @@ -3,10 +3,11 @@ use diesel::pg::Pg; use diesel::prelude::*; use serde::{Deserialize, Serialize}; -use jirs_data::{Project, ProjectCategory, TimeTracking}; +use jirs_data::{Project, ProjectCategory, TimeTracking, UserId}; use crate::db::DbExecutor; use crate::errors::ServiceErrors; +use crate::schema::projects::all_columns; #[derive(Serialize, Deserialize)] pub struct LoadCurrentProject { @@ -83,3 +84,34 @@ impl Handler for DbExecutor { .map_err(|_| ServiceErrors::RecordNotFound("Project".to_string())) } } + +pub struct LoadProjects { + pub user_id: UserId, +} + +impl Message for LoadProjects { + type Result = Result, ServiceErrors>; +} + +impl Handler for DbExecutor { + type Result = Result, ServiceErrors>; + + fn handle(&mut self, msg: LoadProjects, _ctx: &mut Self::Context) -> Self::Result { + use crate::schema::projects::dsl::*; + use crate::schema::user_projects::dsl::{project_id, user_id, user_projects}; + + let conn = &self + .pool + .get() + .map_err(|_| ServiceErrors::DatabaseConnectionLost)?; + + let query = projects + .inner_join(user_projects.on(project_id.eq(id))) + .filter(user_id.eq(msg.user_id)) + .select(all_columns); + debug!("{}", diesel::debug_query::(&query)); + query + .load::(conn) + .map_err(|_| ServiceErrors::RecordNotFound("Project".to_string())) + } +} diff --git a/jirs-server/src/db/user_projects.rs b/jirs-server/src/db/user_projects.rs new file mode 100644 index 00000000..0aed2b77 --- /dev/null +++ b/jirs-server/src/db/user_projects.rs @@ -0,0 +1,111 @@ +use actix::{Handler, Message}; +use diesel::pg::Pg; +use diesel::prelude::*; + +use jirs_data::{UserId, UserProject, UserProjectId}; + +use crate::db::DbExecutor; +use crate::errors::ServiceErrors; + +pub struct CurrentUserProject { + pub user_id: UserId, +} + +impl Message for CurrentUserProject { + type Result = Result; +} + +impl Handler for DbExecutor { + type Result = Result; + + fn handle(&mut self, msg: CurrentUserProject, _: &mut Self::Context) -> Self::Result { + use crate::schema::user_projects::dsl::*; + + let conn = &self + .pool + .get() + .map_err(|_| ServiceErrors::DatabaseConnectionLost)?; + + let user_query = user_projects.filter(user_id.eq(msg.user_id).and(is_current.eq(true))); + debug!("{}", diesel::debug_query::(&user_query)); + user_query + .first(conn) + .map_err(|_e| ServiceErrors::RecordNotFound(format!("user project {}", msg.user_id))) + } +} + +pub struct LoadUserProjects { + pub user_id: UserId, +} + +impl Message for LoadUserProjects { + type Result = Result, ServiceErrors>; +} + +impl Handler for DbExecutor { + type Result = Result, ServiceErrors>; + + fn handle(&mut self, msg: LoadUserProjects, _ctx: &mut Self::Context) -> Self::Result { + use crate::schema::user_projects::dsl::*; + + let conn = &self + .pool + .get() + .map_err(|_| ServiceErrors::DatabaseConnectionLost)?; + + let user_query = user_projects.filter(user_id.eq(msg.user_id)); + debug!("{}", diesel::debug_query::(&user_query)); + user_query + .load(conn) + .map_err(|_e| ServiceErrors::RecordNotFound(format!("user project {}", msg.user_id))) + } +} + +pub struct ChangeCurrentUserProject { + pub user_id: UserId, + pub id: UserProjectId, +} + +impl Message for ChangeCurrentUserProject { + type Result = Result; +} + +impl Handler for DbExecutor { + type Result = Result; + + fn handle(&mut self, msg: ChangeCurrentUserProject, _ctx: &mut Self::Context) -> Self::Result { + use crate::schema::user_projects::dsl::*; + + let conn = &self + .pool + .get() + .map_err(|_| ServiceErrors::DatabaseConnectionLost)?; + + let query = user_projects.filter(id.eq(msg.id).and(user_id.eq(msg.user_id))); + debug!("{}", diesel::debug_query::(&query)); + let mut user_project: UserProject = query + .first(conn) + .map_err(|_e| ServiceErrors::RecordNotFound(format!("user project {}", msg.user_id)))?; + + let query = diesel::update(user_projects) + .set(is_current.eq(false)) + .filter(user_id.eq(msg.user_id)); + debug!("{}", diesel::debug_query::(&query)); + query + .execute(conn) + .map(|_| ()) + .map_err(|_e| ServiceErrors::RecordNotFound(format!("user project {}", msg.user_id)))?; + + let query = diesel::update(user_projects) + .set(is_current.eq(true)) + .filter(id.eq(msg.id).and(user_id.eq(msg.user_id))); + debug!("{}", diesel::debug_query::(&query)); + query + .execute(conn) + .map(|_| ()) + .map_err(|_e| ServiceErrors::RecordNotFound(format!("user project {}", msg.user_id)))?; + + user_project.is_current = true; + Ok(user_project) + } +} diff --git a/jirs-server/src/db/users.rs b/jirs-server/src/db/users.rs index 191edc09..bfba2f5c 100644 --- a/jirs-server/src/db/users.rs +++ b/jirs-server/src/db/users.rs @@ -7,7 +7,8 @@ use jirs_data::{Project, User, UserId}; use crate::db::{DbExecutor, DbPooledConn}; use crate::errors::ServiceErrors; -use crate::models::{CreateProjectForm, UserForm}; +use crate::models::CreateProjectForm; +use crate::schema::users::all_columns; #[derive(Serialize, Deserialize, Debug)] pub struct FindUser { @@ -54,6 +55,7 @@ impl Handler for DbExecutor { type Result = Result, ServiceErrors>; fn handle(&mut self, msg: LoadProjectUsers, _ctx: &mut Self::Context) -> Self::Result { + use crate::schema::user_projects::dsl::{project_id, user_id, user_projects}; use crate::schema::users::dsl::*; let conn = &self @@ -61,7 +63,11 @@ impl Handler for DbExecutor { .get() .map_err(|_| ServiceErrors::DatabaseConnectionLost)?; - let users_query = users.distinct_on(id).filter(project_id.eq(msg.project_id)); + let users_query = users + .distinct_on(id) + .inner_join(user_projects.on(user_id.eq(id))) + .filter(project_id.eq(msg.project_id)) + .select(all_columns); debug!("{}", diesel::debug_query::(&users_query)); users_query .load(conn) @@ -142,19 +148,31 @@ impl Handler for DbExecutor { .get_result(conn) .map_err(|_| ServiceErrors::RegisterCollision)?; - let form = UserForm { - name: msg.name, - email: msg.email, - avatar_url: None, - project_id: project.id, + let user: User = { + let insert_user_query = + diesel::insert_into(users).values((name.eq(msg.name), email.eq(msg.email))); + debug!("{}", diesel::debug_query::(&insert_user_query)); + insert_user_query + .get_result(conn) + .map_err(|_| ServiceErrors::RegisterCollision)? }; - let insert_user_query = diesel::insert_into(users).values(form); - debug!("{}", diesel::debug_query::(&insert_user_query)); - match insert_user_query.execute(conn) { - Ok(_) => (), - _ => return Err(ServiceErrors::RegisterCollision), - }; + { + use crate::schema::user_projects::dsl::*; + let insert_user_project_query = diesel::insert_into(user_projects).values(( + user_id.eq(user.id), + project_id.eq(project.id), + is_current.eq(true), + is_default.eq(true), + )); + debug!( + "{}", + diesel::debug_query::(&insert_user_project_query) + ); + insert_user_project_query + .execute(conn) + .map_err(|_| ServiceErrors::RegisterCollision)?; + } Ok(()) } @@ -287,11 +305,13 @@ mod tests { #[test] fn check_collision() { use crate::schema::projects::dsl::projects; + use crate::schema::user_projects::dsl::user_projects; use crate::schema::users::dsl::users; let pool = build_pool(); let conn = &pool.get().unwrap(); + diesel::delete(user_projects).execute(conn).unwrap(); diesel::delete(users).execute(conn).unwrap(); diesel::delete(projects).execute(conn).unwrap(); @@ -306,16 +326,28 @@ mod tests { .get_result(conn) .unwrap(); - let user_form = UserForm { - name: "Foo".to_string(), - email: "foo@example.com".to_string(), - avatar_url: None, - project_id: project.id, + let user: User = { + use crate::schema::users::dsl::*; + diesel::insert_into(users) + .values(( + name.eq("Foo".to_string()), + email.eq("foo@example.com".to_string()), + )) + .get_result(conn) + .unwrap() }; - diesel::insert_into(users) - .values(user_form) - .execute(conn) - .unwrap(); + { + use crate::schema::user_projects::dsl::*; + diesel::insert_into(user_projects) + .values(( + user_id.eq(user.id), + project_id.eq(project.id), + is_current.eq(true), + is_default.eq(true), + )) + .execute(conn) + .unwrap(); + } assert_eq!(count_matching_users("Foo", "bar@example.com", conn), 1); assert_eq!(count_matching_users("Bar", "foo@example.com", conn), 1); diff --git a/jirs-server/src/models.rs b/jirs-server/src/models.rs index 97e5ab7d..b1651be3 100644 --- a/jirs-server/src/models.rs +++ b/jirs-server/src/models.rs @@ -109,7 +109,6 @@ pub struct UserForm { pub name: String, pub email: String, pub avatar_url: Option, - pub project_id: i32, } #[derive(Debug, Serialize, Deserialize, Insertable)] diff --git a/jirs-server/src/schema.rs b/jirs-server/src/schema.rs index 6a16806e..df84c115 100644 --- a/jirs-server/src/schema.rs +++ b/jirs-server/src/schema.rs @@ -301,6 +301,71 @@ table! { } } +table! { + use diesel::sql_types::*; + use jirs_data::sql::*; + + /// Representation of the `messages` table. + /// + /// (Automatically generated by Diesel.) + messages (id) { + /// The `id` column of the `messages` table. + /// + /// Its SQL type is `Int4`. + /// + /// (Automatically generated by Diesel.) + id -> Int4, + /// The `receiver_id` column of the `messages` table. + /// + /// Its SQL type is `Int4`. + /// + /// (Automatically generated by Diesel.) + receiver_id -> Int4, + /// The `sender_id` column of the `messages` table. + /// + /// Its SQL type is `Int4`. + /// + /// (Automatically generated by Diesel.) + sender_id -> Int4, + /// The `summary` column of the `messages` table. + /// + /// Its SQL type is `Text`. + /// + /// (Automatically generated by Diesel.) + summary -> Text, + /// The `description` column of the `messages` table. + /// + /// Its SQL type is `Text`. + /// + /// (Automatically generated by Diesel.) + description -> Text, + /// The `message_type` column of the `messages` table. + /// + /// Its SQL type is `Text`. + /// + /// (Automatically generated by Diesel.) + message_type -> Text, + /// The `hyper_link` column of the `messages` table. + /// + /// Its SQL type is `Text`. + /// + /// (Automatically generated by Diesel.) + hyper_link -> Text, + /// The `created_at` column of the `messages` table. + /// + /// Its SQL type is `Timestamp`. + /// + /// (Automatically generated by Diesel.) + created_at -> Timestamp, + /// The `updated_at` column of the `messages` table. + /// + /// Its SQL type is `Timestamp`. + /// + /// (Automatically generated by Diesel.) + updated_at -> Timestamp, + } +} + table! { use diesel::sql_types::*; use jirs_data::sql::*; @@ -413,6 +478,65 @@ table! { } } +table! { + use diesel::sql_types::*; + use jirs_data::sql::*; + + /// Representation of the `user_projects` table. + /// + /// (Automatically generated by Diesel.) + user_projects (id) { + /// The `id` column of the `user_projects` table. + /// + /// Its SQL type is `Int4`. + /// + /// (Automatically generated by Diesel.) + id -> Int4, + /// The `user_id` column of the `user_projects` table. + /// + /// Its SQL type is `Int4`. + /// + /// (Automatically generated by Diesel.) + user_id -> Int4, + /// The `project_id` column of the `user_projects` table. + /// + /// Its SQL type is `Int4`. + /// + /// (Automatically generated by Diesel.) + project_id -> Int4, + /// The `is_default` column of the `user_projects` table. + /// + /// Its SQL type is `Bool`. + /// + /// (Automatically generated by Diesel.) + is_default -> Bool, + /// The `is_current` column of the `user_projects` table. + /// + /// Its SQL type is `Bool`. + /// + /// (Automatically generated by Diesel.) + is_current -> Bool, + /// The `role` column of the `user_projects` table. + /// + /// Its SQL type is `UserRoleType`. + /// + /// (Automatically generated by Diesel.) + role -> UserRoleType, + /// The `created_at` column of the `user_projects` table. + /// + /// Its SQL type is `Timestamp`. + /// + /// (Automatically generated by Diesel.) + created_at -> Timestamp, + /// The `updated_at` column of the `user_projects` table. + /// + /// Its SQL type is `Timestamp`. + /// + /// (Automatically generated by Diesel.) + updated_at -> Timestamp, + } +} + table! { use diesel::sql_types::*; use jirs_data::sql::*; @@ -445,12 +569,6 @@ table! { /// /// (Automatically generated by Diesel.) avatar_url -> Nullable, - /// The `project_id` column of the `users` table. - /// - /// Its SQL type is `Int4`. - /// - /// (Automatically generated by Diesel.) - project_id -> Int4, /// The `created_at` column of the `users` table. /// /// Its SQL type is `Timestamp`. @@ -463,12 +581,6 @@ table! { /// /// (Automatically generated by Diesel.) updated_at -> Timestamp, - /// The `role` column of the `users` table. - /// - /// Its SQL type is `UserRoleType`. - /// - /// (Automatically generated by Diesel.) - role -> UserRoleType, } } @@ -483,7 +595,8 @@ joinable!(issues -> issue_statuses (issue_status_id)); joinable!(issues -> projects (project_id)); joinable!(issues -> users (reporter_id)); joinable!(tokens -> users (user_id)); -joinable!(users -> projects (project_id)); +joinable!(user_projects -> projects (project_id)); +joinable!(user_projects -> users (user_id)); allow_tables_to_appear_in_same_query!( comments, @@ -491,7 +604,9 @@ allow_tables_to_appear_in_same_query!( issue_assignees, issues, issue_statuses, + messages, projects, tokens, + user_projects, users, ); diff --git a/jirs-server/src/web/avatar.rs b/jirs-server/src/web/avatar.rs index 385edb6f..a8c67bb9 100644 --- a/jirs-server/src/web/avatar.rs +++ b/jirs-server/src/web/avatar.rs @@ -9,6 +9,7 @@ use actix_multipart::{Field, Multipart}; use actix_web::http::header::ContentDisposition; use actix_web::web::Data; use actix_web::{post, web, Error, HttpResponse}; +use futures::executor::block_on; use futures::{StreamExt, TryStreamExt}; #[cfg(feature = "aws-s3")] use rusoto_s3::{PutObjectRequest, S3Client, S3}; @@ -16,6 +17,7 @@ use rusoto_s3::{PutObjectRequest, S3Client, S3}; use jirs_data::{User, UserId, WsMsg}; use crate::db::authorize_user::AuthorizeUser; +use crate::db::user_projects::CurrentUserProject; use crate::db::users::UpdateAvatarUrl; use crate::db::DbExecutor; #[cfg(feature = "aws-s3")] @@ -51,11 +53,21 @@ pub async fn upload( _ => continue, }; } + let user_id = match user_id { + Some(id) => id, + _ => return Ok(HttpResponse::Unauthorized().finish()), + }; + + let project_id = match block_on(db.send(CurrentUserProject { user_id })) { + Ok(Ok(user_project)) => user_project.project_id, + _ => return Ok(HttpResponse::UnprocessableEntity().finish()), + }; + match (user_id, avatar_url) { - (Some(user_id), Some(avatar_url)) => { + (user_id, Some(avatar_url)) => { let user = update_user_avatar(user_id, avatar_url.clone(), db).await?; ws.send(BroadcastToChannel( - user.project_id, + project_id, WsMsg::AvatarUrlChanged(user.id, avatar_url), )) .await diff --git a/jirs-server/src/ws/auth.rs b/jirs-server/src/ws/auth.rs index 047da450..d47033f4 100644 --- a/jirs-server/src/ws/auth.rs +++ b/jirs-server/src/ws/auth.rs @@ -78,6 +78,9 @@ impl WsHandler for WebSocketActor { _ => return Ok(Some(WsMsg::AuthorizeExpired)), }; self.current_user = Some(user.clone()); + self.current_user_project = self.load_user_project().ok(); + self.current_project = self.load_project().ok(); + block_on(self.join_channel(ctx.address().recipient())); Ok(Some(WsMsg::AuthorizeLoaded(Ok(user)))) } diff --git a/jirs-server/src/ws/comments.rs b/jirs-server/src/ws/comments.rs index 8469375b..b57154f1 100644 --- a/jirs-server/src/ws/comments.rs +++ b/jirs-server/src/ws/comments.rs @@ -16,7 +16,14 @@ impl WsHandler for WebSocketActor { issue_id: msg.issue_id, })) { Ok(Ok(comments)) => comments, - _ => return Ok(None), + Ok(Err(e)) => { + error!("{:?}", e); + return Ok(None); + } + Err(e) => { + error!("{}", e); + return Ok(None); + } }; Ok(Some(WsMsg::IssueCommentsLoaded(comments))) @@ -38,7 +45,14 @@ impl WsHandler for WebSocketActor { body: msg.body, })) { Ok(Ok(_)) => (), - _ => return Ok(None), + Ok(Err(e)) => { + error!("{:?}", e); + return Ok(None); + } + Err(e) => { + error!("{}", e); + return Ok(None); + } }; self.handle_msg(LoadIssueComments { issue_id }, ctx) } @@ -62,7 +76,14 @@ impl WsHandler for WebSocketActor { body, })) { Ok(Ok(comment)) => comment.issue_id, - _ => return Ok(None), + Ok(Err(e)) => { + error!("{:?}", e); + return Ok(None); + } + Err(e) => { + error!("{}", e); + return Ok(None); + } }; if let Some(v) = self.handle_msg(LoadIssueComments { issue_id }, ctx)? { self.broadcast(&v); @@ -87,7 +108,14 @@ impl WsHandler for WebSocketActor { }; match block_on(self.db.send(m)) { Ok(Ok(_)) => (), - _ => return Ok(None), + Ok(Err(e)) => { + error!("{:?}", e); + return Ok(None); + } + Err(e) => { + error!("{}", e); + return Ok(None); + } }; Ok(Some(WsMsg::CommentDeleted(msg.comment_id))) diff --git a/jirs-server/src/ws/invitations.rs b/jirs-server/src/ws/invitations.rs index bf3b3509..1f1b939f 100644 --- a/jirs-server/src/ws/invitations.rs +++ b/jirs-server/src/ws/invitations.rs @@ -15,7 +15,14 @@ impl WsHandler for WebSocketActor { }; let res = match block_on(self.db.send(invitations::ListInvitation { user_id })) { Ok(Ok(v)) => Some(WsMsg::InvitationListLoaded(v)), - _ => None, + Ok(Err(e)) => { + error!("{:?}", e); + return Ok(None); + } + Err(e) => { + error!("{}", e); + return Ok(None); + } }; Ok(res) } @@ -28,14 +35,15 @@ pub struct CreateInvitation { impl WsHandler for WebSocketActor { fn handle_msg(&mut self, msg: CreateInvitation, _ctx: &mut Self::Context) -> WsResult { - let (user_id, inviter_name, project_id) = match self - .current_user - .as_ref() - .map(|u| (u.id, u.name.clone(), u.project_id)) - { - Some(id) => id, + let project_id = match self.current_user_project.as_ref() { + Some(up) => up.project_id, _ => return Ok(None), }; + let (user_id, inviter_name) = + match self.current_user.as_ref().map(|u| (u.id, u.name.clone())) { + Some(id) => id, + _ => return Ok(None), + }; let CreateInvitation { email, name } = msg; let invitation = match block_on(self.db.send(invitations::CreateInvitation { @@ -84,7 +92,14 @@ impl WsHandler for WebSocketActor { let DeleteInvitation { id } = msg; let res = match block_on(self.db.send(invitations::DeleteInvitation { id })) { Ok(Ok(_)) => None, - _ => None, + Ok(Err(e)) => { + error!("{:?}", e); + return Ok(None); + } + Err(e) => { + error!("{}", e); + return Ok(None); + } }; Ok(res) } @@ -100,7 +115,14 @@ impl WsHandler for WebSocketActor { let RevokeInvitation { id } = msg; let res = match block_on(self.db.send(invitations::RevokeInvitation { id })) { Ok(Ok(_)) => Some(WsMsg::InvitationRevokeSuccess(id)), - _ => None, + Ok(Err(e)) => { + error!("{:?}", e); + return Ok(None); + } + Err(e) => { + error!("{}", e); + return Ok(None); + } }; Ok(res) } @@ -116,7 +138,14 @@ impl WsHandler for WebSocketActor { let AcceptInvitation { id } = msg; let res = match block_on(self.db.send(invitations::AcceptInvitation { id })) { Ok(Ok(_)) => Some(WsMsg::InvitationAcceptSuccess(id)), - _ => None, + Ok(Err(e)) => { + error!("{:?}", e); + return Ok(None); + } + Err(e) => { + error!("{}", e); + return Ok(None); + } }; Ok(res) } diff --git a/jirs-server/src/ws/issue_statuses.rs b/jirs-server/src/ws/issue_statuses.rs index a7fc4505..0a734484 100644 --- a/jirs-server/src/ws/issue_statuses.rs +++ b/jirs-server/src/ws/issue_statuses.rs @@ -9,14 +9,21 @@ pub struct LoadIssueStatuses; impl WsHandler for WebSocketActor { fn handle_msg(&mut self, _msg: LoadIssueStatuses, _ctx: &mut Self::Context) -> WsResult { - let project_id = self.require_user()?.project_id; + let project_id = self.require_user_project()?.project_id; let msg = match block_on( self.db .send(issue_statuses::LoadIssueStatuses { project_id }), ) { Ok(Ok(v)) => Some(WsMsg::IssueStatusesResponse(v)), - _ => None, + Ok(Err(e)) => { + error!("{:?}", e); + return Ok(None); + } + Err(e) => { + error!("{}", e); + return Ok(None); + } }; Ok(msg) } @@ -29,7 +36,7 @@ pub struct CreateIssueStatus { impl WsHandler for WebSocketActor { fn handle_msg(&mut self, msg: CreateIssueStatus, _ctx: &mut Self::Context) -> WsResult { - let project_id = self.require_user()?.project_id; + let project_id = self.require_user_project()?.project_id; let CreateIssueStatus { position, name } = msg; let msg = match block_on(self.db.send(issue_statuses::CreateIssueStatus { @@ -38,7 +45,14 @@ impl WsHandler for WebSocketActor { name, })) { Ok(Ok(is)) => Some(WsMsg::IssueStatusCreated(is)), - _ => None, + Ok(Err(e)) => { + error!("{:?}", e); + return Ok(None); + } + Err(e) => { + error!("{}", e); + return Ok(None); + } }; Ok(msg) } @@ -50,7 +64,7 @@ pub struct DeleteIssueStatus { impl WsHandler for WebSocketActor { fn handle_msg(&mut self, msg: DeleteIssueStatus, _ctx: &mut Self::Context) -> WsResult { - let project_id = self.require_user()?.project_id; + let project_id = self.require_user_project()?.project_id; let DeleteIssueStatus { issue_status_id } = msg; let msg = match block_on(self.db.send(issue_statuses::DeleteIssueStatus { @@ -58,7 +72,14 @@ impl WsHandler for WebSocketActor { project_id, })) { Ok(Ok(is)) => Some(WsMsg::IssueStatusDeleted(is)), - _ => None, + Ok(Err(e)) => { + error!("{:?}", e); + return Ok(None); + } + Err(e) => { + error!("{}", e); + return Ok(None); + } }; Ok(msg) } @@ -72,7 +93,7 @@ pub struct UpdateIssueStatus { impl WsHandler for WebSocketActor { fn handle_msg(&mut self, msg: UpdateIssueStatus, _ctx: &mut Self::Context) -> WsResult { - let project_id = self.require_user()?.project_id; + let project_id = self.require_user_project()?.project_id; let UpdateIssueStatus { issue_status_id, @@ -86,7 +107,14 @@ impl WsHandler for WebSocketActor { project_id, })) { Ok(Ok(is)) => Some(WsMsg::IssueStatusUpdated(is)), - _ => None, + Ok(Err(e)) => { + error!("{:?}", e); + return Ok(None); + } + Err(e) => { + error!("{}", e); + return Ok(None); + } }; if let Some(ws_msg) = msg.as_ref() { self.broadcast(ws_msg) diff --git a/jirs-server/src/ws/issues.rs b/jirs-server/src/ws/issues.rs index 2da76ea9..ef9c0ff1 100644 --- a/jirs-server/src/ws/issues.rs +++ b/jirs-server/src/ws/issues.rs @@ -71,7 +71,14 @@ impl WsHandler for WebSocketActor { let assignees: Vec = match block_on(self.db.send(LoadAssignees { issue_id: issue.id })) { Ok(Ok(v)) => v, - _ => return Ok(None), + Ok(Err(e)) => { + error!("{:?}", e); + return Ok(None); + } + Err(e) => { + error!("{}", e); + return Ok(None); + } }; for assignee in assignees { @@ -102,7 +109,14 @@ impl WsHandler for WebSocketActor { }; let m = match block_on(self.db.send(msg)) { Ok(Ok(issue)) => Some(WsMsg::IssueCreated(issue.into())), - _ => None, + Ok(Err(e)) => { + error!("{:?}", e); + return Ok(None); + } + Err(e) => { + error!("{}", e); + return Ok(None); + } }; Ok(m) } @@ -120,7 +134,14 @@ impl WsHandler for WebSocketActor { .send(crate::db::issues::DeleteIssue { issue_id: msg.id }), ) { Ok(Ok(_)) => Some(WsMsg::IssueDeleted(msg.id)), - _ => None, + Ok(Err(e)) => { + error!("{:?}", e); + return Ok(None); + } + Err(e) => { + error!("{}", e); + return Ok(None); + } }; Ok(m) } @@ -130,7 +151,7 @@ pub struct LoadIssues; impl WsHandler for WebSocketActor { fn handle_msg(&mut self, _msg: LoadIssues, _ctx: &mut Self::Context) -> WsResult { - let project_id = self.require_user()?.project_id; + let project_id = self.require_user_project()?.project_id; let issues: Vec = match block_on(self.db.send(LoadProjectIssues { project_id })) { diff --git a/jirs-server/src/ws/mod.rs b/jirs-server/src/ws/mod.rs index d64b3852..07285104 100644 --- a/jirs-server/src/ws/mod.rs +++ b/jirs-server/src/ws/mod.rs @@ -6,9 +6,12 @@ use actix::{ use actix_web::web::Data; use actix_web::{get, web, Error, HttpRequest, HttpResponse}; use actix_web_actors::ws; +use futures::executor::block_on; -use jirs_data::{ProjectId, UserId, WsMsg}; +use jirs_data::{Project, ProjectId, User, UserId, UserProject, WsMsg}; +use crate::db::projects::LoadCurrentProject; +use crate::db::user_projects::CurrentUserProject; use crate::db::DbExecutor; use crate::mail::MailExecutor; use crate::ws::auth::*; @@ -25,6 +28,7 @@ pub mod invitations; pub mod issue_statuses; pub mod issues; pub mod projects; +pub mod user_projects; pub mod users; pub type WsResult = std::result::Result, WsMsg>; @@ -36,8 +40,10 @@ trait WsMessageSender { struct WebSocketActor { db: Data>, mail: Data>, - current_user: Option, addr: Addr, + current_user: Option, + current_user_project: Option, + current_project: Option, } impl Actor for WebSocketActor { @@ -62,12 +68,12 @@ impl Handler for WebSocketActor { impl WebSocketActor { fn broadcast(&self, msg: &WsMsg) { - let user = match self.current_user.as_ref() { - Some(u) => u, + let project_id = match self.require_user_project() { + Ok(up) => up.project_id, _ => return, }; self.addr - .do_send(InnerMsg::BroadcastToChannel(user.project_id, msg.clone())); + .do_send(InnerMsg::BroadcastToChannel(project_id, msg.clone())); } fn handle_ws_msg( @@ -179,13 +185,18 @@ impl WebSocketActor { async fn join_channel(&self, addr: Recipient) { info!("joining channel..."); info!(" current user {:?}", self.current_user); + let user = match self.current_user.as_ref() { None => return, Some(u) => u, }; + let project_id = match self.require_user_project() { + Ok(user_project) => user_project.project_id, + _ => return, + }; match self .addr - .send(InnerMsg::Join(user.project_id, user.id, addr)) + .send(InnerMsg::Join(project_id, user.id, addr)) .await { Err(e) => error!("{}", e), @@ -193,12 +204,42 @@ impl WebSocketActor { }; } - fn require_user(&self) -> Result<&jirs_data::User, WsMsg> { + fn require_user(&self) -> Result<&User, WsMsg> { self.current_user .as_ref() .map(|u| u) .ok_or_else(|| WsMsg::AuthorizeExpired) } + + fn require_user_project(&self) -> Result<&UserProject, WsMsg> { + self.current_user_project + .as_ref() + .map(|u| u) + .ok_or_else(|| WsMsg::AuthorizeExpired) + } + + // fn require_project(&self) -> Result<&Project, WsMsg> { + // self.current_project + // .as_ref() + // .map(|u| u) + // .ok_or_else(|| WsMsg::AuthorizeExpired) + // } + + fn load_user_project(&self) -> Result { + let user_id = self.require_user().map_err(|_| WsMsg::AuthorizeExpired)?.id; + match block_on(self.db.send(CurrentUserProject { user_id })) { + Ok(Ok(user_project)) => Ok(user_project), + _ => Err(WsMsg::AuthorizeExpired), + } + } + + fn load_project(&self) -> Result { + let project_id = self.require_user_project()?.project_id; + match block_on(self.db.send(LoadCurrentProject { project_id })) { + Ok(Ok(project)) => Ok(project), + _ => Err(WsMsg::AuthorizeExpired), + } + } } impl StreamHandler> for WebSocketActor { @@ -226,9 +267,12 @@ impl StreamHandler> for WebSocketActor { fn finished(&mut self, ctx: &mut Self::Context) { info!("Disconnected"); - if let Some(user) = self.current_user.as_ref() { + if let (Some(user), Some(up)) = ( + self.current_user.as_ref(), + self.current_user_project.as_ref(), + ) { self.addr.do_send(InnerMsg::Leave( - user.project_id, + up.project_id, user.id, ctx.address().recipient(), )); @@ -353,6 +397,8 @@ pub async fn index( db, mail, current_user: None, + current_user_project: None, + current_project: None, addr: ws_server.get_ref().clone(), }, &req, diff --git a/jirs-server/src/ws/projects.rs b/jirs-server/src/ws/projects.rs index e128b109..25139552 100644 --- a/jirs-server/src/ws/projects.rs +++ b/jirs-server/src/ws/projects.rs @@ -9,7 +9,7 @@ pub struct CurrentProject; impl WsHandler for WebSocketActor { fn handle_msg(&mut self, _msg: CurrentProject, _ctx: &mut Self::Context) -> WsResult { - let project_id = self.require_user()?.project_id; + let project_id = self.require_user_project()?.project_id; let m = match block_on(self.db.send(LoadCurrentProject { project_id })) { Ok(Ok(project)) => Some(WsMsg::ProjectLoaded(project)), @@ -28,7 +28,7 @@ impl WsHandler for WebSocketActor { impl WsHandler for WebSocketActor { fn handle_msg(&mut self, msg: UpdateProjectPayload, _ctx: &mut Self::Context) -> WsResult { - let project_id = self.require_user()?.project_id; + let project_id = self.require_user_project()?.project_id; let project = match block_on(self.db.send(crate::db::projects::UpdateProject { project_id, name: msg.name, diff --git a/jirs-server/src/ws/user_projects.rs b/jirs-server/src/ws/user_projects.rs new file mode 100644 index 00000000..a95ec92d --- /dev/null +++ b/jirs-server/src/ws/user_projects.rs @@ -0,0 +1,52 @@ +use futures::executor::block_on; + +use jirs_data::{UserProjectId, WsMsg}; + +use crate::db; +use crate::ws::{WebSocketActor, WsHandler, WsResult}; + +pub struct LoadUserProjects; + +impl WsHandler for WebSocketActor { + fn handle_msg(&mut self, _msg: LoadUserProjects, _ctx: &mut Self::Context) -> WsResult { + let user_id = self.require_user()?.id; + match block_on( + self.db + .send(db::user_projects::LoadUserProjects { user_id }), + ) { + Ok(Ok(v)) => Ok(Some(WsMsg::UserProjectLoaded(v))), + Ok(Err(e)) => { + error!("{:?}", e); + return Ok(None); + } + Err(e) => { + error!("{}", e); + return Ok(None); + } + } + } +} + +pub struct SetCurrentUserProject { + pub id: UserProjectId, +} + +impl WsHandler for WebSocketActor { + fn handle_msg(&mut self, msg: SetCurrentUserProject, _ctx: &mut Self::Context) -> WsResult { + let user_id = self.require_user()?.id; + match block_on(self.db.send(db::user_projects::ChangeCurrentUserProject { + user_id, + id: msg.id, + })) { + Ok(Ok(user_project)) => Ok(Some(WsMsg::UserProjectCurrentChanged(user_project))), + Ok(Err(e)) => { + error!("{:?}", e); + return Ok(None); + } + Err(e) => { + error!("{}", e); + return Ok(None); + } + } + } +} diff --git a/jirs-server/src/ws/users.rs b/jirs-server/src/ws/users.rs index d934825a..cc8763fa 100644 --- a/jirs-server/src/ws/users.rs +++ b/jirs-server/src/ws/users.rs @@ -12,10 +12,17 @@ impl WsHandler for WebSocketActor { fn handle_msg(&mut self, _msg: LoadProjectUsers, _ctx: &mut Self::Context) -> WsResult { use crate::db::users::LoadProjectUsers as Msg; - let project_id = self.require_user()?.project_id; + let project_id = self.require_user_project()?.project_id; let m = match block_on(self.db.send(Msg { project_id })) { Ok(Ok(v)) => Some(WsMsg::ProjectUsersLoaded(v)), - _ => None, + Ok(Err(e)) => { + error!("{:?}", e); + return Ok(None); + } + Err(e) => { + error!("{}", e); + return Ok(None); + } }; Ok(m) } @@ -35,7 +42,10 @@ impl WsHandler for WebSocketActor { })) { Ok(Ok(_)) => Some(WsMsg::SignUpSuccess), Ok(Err(_)) => Some(WsMsg::SignUpPairTaken), - _ => None, + Err(e) => { + error!("{}", e); + return Ok(None); + } }; match self.handle_msg(Authenticate { name, email }, ctx) { @@ -78,7 +88,14 @@ impl WsHandler for WebSocketActor { email, })) { Ok(Ok(_users)) => (), - _ => return Ok(None), + Ok(Err(e)) => { + error!("{:?}", e); + return Ok(None); + } + Err(e) => { + error!("{}", e); + return Ok(None); + } }; Ok(Some(WsMsg::ProfileUpdated))