Add multi project support.

This commit is contained in:
Adrian Wozniak 2020-05-21 17:02:16 +02:00
parent 2d55c5f143
commit 372b9d9b8d
36 changed files with 859 additions and 156 deletions

9
.cargo/config Normal file
View File

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

View File

@ -12,7 +12,7 @@ use crate::{FieldId, Msg};
pub fn update(msg: Msg, model: &mut Model, _orders: &mut impl Orders<Msg>) { pub fn update(msg: Msg, model: &mut Model, _orders: &mut impl Orders<Msg>) {
if let Msg::ChangePage(Page::Project) = msg { if let Msg::ChangePage(Page::Project) = msg {
model.page_content = PageContent::Invite(Box::new(InvitePage::default())); build_page_content(model);
return; return;
} }
@ -27,6 +27,10 @@ pub fn update(msg: Msg, model: &mut Model, _orders: &mut impl Orders<Msg>) {
} }
} }
fn build_page_content(model: &mut Model) {
model.page_content = PageContent::Invite(Box::new(InvitePage::default()));
}
pub fn view(model: &Model) -> Node<Msg> { pub fn view(model: &Model) -> Node<Msg> {
let page = match &model.page_content { let page = match &model.page_content {
PageContent::Invite(page) => page, PageContent::Invite(page) => page,

View File

@ -175,6 +175,7 @@ fn update(msg: Msg, model: &mut model::Model, orders: &mut impl Orders<Msg>) {
_ => (), _ => (),
} }
crate::modal::update(&msg, model, orders); crate::modal::update(&msg, model, orders);
crate::shared::aside::update(&msg, model, orders);
match model.page { match model.page {
Page::Project | Page::AddIssue | Page::EditIssue(..) => project::update(msg, model, orders), Page::Project | Page::AddIssue | Page::EditIssue(..) => project::update(msg, model, orders),
Page::ProjectSettings => project_settings::update(msg, model, orders), Page::ProjectSettings => project_settings::update(msg, model, orders),

View File

@ -458,11 +458,14 @@ pub struct Model {
pub project: Option<Project>, pub project: Option<Project>,
pub user: Option<User>, pub user: Option<User>,
pub current_user_project: Option<UserProject>,
pub issues: Vec<Issue>, pub issues: Vec<Issue>,
pub users: Vec<User>, pub users: Vec<User>,
pub comments: Vec<Comment>, pub comments: Vec<Comment>,
pub issue_statuses: Vec<IssueStatus>, pub issue_statuses: Vec<IssueStatus>,
pub messages: Vec<Message>, pub messages: Vec<Message>,
pub user_projects: Vec<UserProject>,
pub projects: Vec<Project>,
} }
impl Model { impl Model {
@ -475,8 +478,6 @@ impl Model {
issue_form: None, issue_form: None,
project_form: None, project_form: None,
comment_form: None, comment_form: None,
issues: vec![],
users: vec![],
comments_by_project_id: Default::default(), comments_by_project_id: Default::default(),
page: Page::Project, page: Page::Project,
host_url, host_url,
@ -484,9 +485,12 @@ impl Model {
page_content: PageContent::Project(Box::new(ProjectPage::default())), page_content: PageContent::Project(Box::new(ProjectPage::default())),
modals: vec![], modals: vec![],
project: None, project: None,
comments: vec![], current_user_project: None,
about_tooltip_visible: false, about_tooltip_visible: false,
messages_tooltip_visible: false, messages_tooltip_visible: false,
issues: vec![],
users: vec![],
comments: vec![],
issue_statuses: vec![], issue_statuses: vec![],
messages: vec![Message { messages: vec![Message {
id: 0, id: 0,
@ -499,6 +503,22 @@ impl Model {
created_at: chrono::NaiveDateTime::from_timestamp(4567890, 123), created_at: chrono::NaiveDateTime::from_timestamp(4567890, 123),
updated_at: chrono::NaiveDateTime::from_timestamp(1234567, 098), 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()
}
} }

View File

@ -17,12 +17,8 @@ pub fn update(msg: Msg, model: &mut crate::model::Model, orders: &mut impl Order
match msg { match msg {
Msg::WebSocketChange(WebSocketChanged::WsMsg(WsMsg::AuthorizeLoaded(..))) Msg::WebSocketChange(WebSocketChanged::WsMsg(WsMsg::AuthorizeLoaded(..)))
| Msg::ChangePage(Page::Profile) => { | Msg::ChangePage(Page::Profile) => {
send_ws_msg(WsMsg::ProjectRequest, model.ws.as_ref(), orders); init_load(model, orders);
let user = match model.user { build_page_content(model);
Some(ref user) => user,
_ => return,
};
model.page_content = PageContent::Profile(Box::new(ProfilePage::new(user)));
} }
_ => (), _ => (),
} }
@ -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<Msg>) {
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<Msg> { pub fn view(model: &Model) -> Node<Msg> {
let page = match &model.page_content { let page = match &model.page_content {
PageContent::Profile(profile_page) => profile_page, PageContent::Profile(profile_page) => profile_page,

View File

@ -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::Project)
| Msg::ChangePage(Page::AddIssue) | Msg::ChangePage(Page::AddIssue)
| Msg::ChangePage(Page::EditIssue(..)) => { | 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::Project)
| Msg::ChangePage(Page::AddIssue) | Msg::ChangePage(Page::AddIssue)
| Msg::ChangePage(Page::EditIssue(..)) => { | Msg::ChangePage(Page::EditIssue(..)) => {
enqueue_ws_msg( init_load(model, orders);
vec![
jirs_data::WsMsg::ProjectRequest,
jirs_data::WsMsg::ProjectIssuesRequest,
jirs_data::WsMsg::ProjectUsersRequest,
jirs_data::WsMsg::IssueStatusesRequest,
],
model.ws.as_ref(),
orders,
);
} }
Msg::WebSocketChange(WebSocketChanged::WsMsg(WsMsg::IssueUpdated(issue))) => { Msg::WebSocketChange(WebSocketChanged::WsMsg(WsMsg::IssueUpdated(issue))) => {
let mut old: Vec<Issue> = vec![]; let mut old: Vec<Issue> = 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<Msg>) {
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<Msg> { pub fn view(model: &Model) -> Node<Msg> {
let project_section = vec![ let project_section = vec![
breadcrumbs(model), breadcrumbs(model),

View File

@ -391,15 +391,7 @@ pub fn update(msg: Msg, model: &mut model::Model, orders: &mut impl Orders<Msg>)
match msg { match msg {
Msg::WebSocketChange(ref change) => match change { Msg::WebSocketChange(ref change) => match change {
WebSocketChanged::WsMsg(WsMsg::AuthorizeLoaded(..)) => { WebSocketChanged::WsMsg(WsMsg::AuthorizeLoaded(..)) => {
enqueue_ws_msg( init_load(model, orders);
vec![
WsMsg::ProjectRequest,
WsMsg::IssueStatusesRequest,
WsMsg::ProjectIssuesRequest,
],
model.ws.as_ref(),
orders,
);
} }
WebSocketChanged::WsMsg(WsMsg::ProjectLoaded(..)) => { WebSocketChanged::WsMsg(WsMsg::ProjectLoaded(..)) => {
build_page_content(model); build_page_content(model);
@ -417,15 +409,7 @@ pub fn update(msg: Msg, model: &mut model::Model, orders: &mut impl Orders<Msg>)
Msg::ChangePage(Page::ProjectSettings) => { Msg::ChangePage(Page::ProjectSettings) => {
build_page_content(model); build_page_content(model);
if model.user.is_some() { if model.user.is_some() {
enqueue_ws_msg( init_load(model, orders);
vec![
WsMsg::ProjectRequest,
WsMsg::IssueStatusesRequest,
WsMsg::ProjectIssuesRequest,
],
model.ws.as_ref(),
orders,
);
} }
} }
_ => (), _ => (),
@ -547,6 +531,18 @@ pub fn update(msg: Msg, model: &mut model::Model, orders: &mut impl Orders<Msg>)
} }
} }
fn init_load(model: &mut Model, orders: &mut impl Orders<Msg>) {
enqueue_ws_msg(
vec![
WsMsg::ProjectRequest,
WsMsg::IssueStatusesRequest,
WsMsg::ProjectIssuesRequest,
],
model.ws.as_ref(),
orders,
);
}
fn exchange_position(bellow_id: IssueStatusId, model: &mut Model) { fn exchange_position(bellow_id: IssueStatusId, model: &mut Model) {
let page = match &mut model.page_content { let page = match &mut model.page_content {
PageContent::ProjectSettings(page) => page, PageContent::ProjectSettings(page) => page,

View File

@ -1,11 +1,25 @@
use seed::{prelude::*, *}; use seed::{prelude::*, *};
use jirs_data::UserRole; use jirs_data::{UserRole, WsMsg};
use crate::model::{Model, Page}; use crate::model::{Model, Page};
use crate::shared::styled_icon::{Icon, StyledIcon}; use crate::shared::styled_icon::{Icon, StyledIcon};
use crate::shared::{divider, ToNode}; 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<Msg>) {
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<Msg> { pub fn render(model: &Model) -> Node<Msg> {
let project_icon = Node::from_html(include_str!("../../static/project-avatar.svg")); let project_icon = Node::from_html(include_str!("../../static/project-avatar.svg"));
@ -36,7 +50,7 @@ pub fn render(model: &Model) -> Node<Msg> {
sidebar_link_item(model, "Components", Icon::Component, None), 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( links.push(sidebar_link_item(
model, model,
"Users", "Users",

View File

@ -5,7 +5,7 @@ use uuid::Uuid;
use jirs_data::WsMsg; 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_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;
@ -24,7 +24,7 @@ pub fn update(msg: Msg, model: &mut model::Model, orders: &mut impl Orders<Msg>)
match msg { match msg {
Msg::ChangePage(Page::SignIn) => { Msg::ChangePage(Page::SignIn) => {
model.page_content = PageContent::SignIn(Box::new(SignInPage::default())); build_page_content(model);
return; return;
} }
_ => (), _ => (),
@ -85,6 +85,10 @@ pub fn update(msg: Msg, model: &mut model::Model, orders: &mut impl Orders<Msg>)
}; };
} }
fn build_page_content(model: &mut Model) {
model.page_content = PageContent::SignIn(Box::new(SignInPage::default()));
}
pub fn view(model: &model::Model) -> Node<Msg> { pub fn view(model: &model::Model) -> Node<Msg> {
let page = match &model.page_content { let page = match &model.page_content {
PageContent::SignIn(page) => page, PageContent::SignIn(page) => page,

View File

@ -2,7 +2,7 @@ use seed::{prelude::*, *};
use jirs_data::{SignUpFieldId, WsMsg}; 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_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;
@ -21,7 +21,7 @@ pub fn update(msg: Msg, model: &mut model::Model, orders: &mut impl Orders<Msg>)
match msg { match msg {
Msg::ChangePage(Page::SignUp) => { Msg::ChangePage(Page::SignUp) => {
model.page_content = PageContent::SignUp(Box::new(SignUpPage::default())); build_page_content(model);
return; return;
} }
_ => (), _ => (),
@ -61,6 +61,10 @@ pub fn update(msg: Msg, model: &mut model::Model, orders: &mut impl Orders<Msg>)
} }
} }
fn build_page_content(model: &mut Model) {
model.page_content = PageContent::SignUp(Box::new(SignUpPage::default()));
}
pub fn view(model: &model::Model) -> Node<Msg> { pub fn view(model: &model::Model) -> Node<Msg> {
let page = match &model.page_content { let page = match &model.page_content {
PageContent::SignUp(page) => page, PageContent::SignUp(page) => page,

View File

@ -231,11 +231,12 @@ pub fn view(model: &Model) -> Node<Msg> {
.on_click(mouse_ev(Ev::Click, move |_| Msg::InvitedUserRemove(email))) .on_click(mouse_ev(Ev::Click, move |_| Msg::InvitedUserRemove(email)))
.build() .build()
.into_node(); .into_node();
// span![format!("{}", user.user_role)],
li![ li![
class!["user"], class!["user"],
span![user.name.as_str()], span![user.name.as_str()],
span![user.email.as_str()], span![user.email.as_str()],
span![format!("{}", user.user_role)],
remove, remove,
] ]
}) })

View File

@ -90,6 +90,33 @@ pub fn update(msg: &WsMsg, model: &mut Model, orders: &mut impl Orders<Msg>) {
WsMsg::ProjectLoaded(project) => { WsMsg::ProjectLoaded(project) => {
model.project = Some(project.clone()); 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 // issues
WsMsg::ProjectIssuesLoaded(v) => { WsMsg::ProjectIssuesLoaded(v) => {
let mut v = v.clone(); let mut v = v.clone();

View File

@ -21,6 +21,7 @@ pub trait ToVec {
pub type IssueId = i32; pub type IssueId = i32;
pub type ProjectId = i32; pub type ProjectId = i32;
pub type UserId = i32; pub type UserId = i32;
pub type UserProjectId = i32;
pub type CommentId = i32; pub type CommentId = i32;
pub type TokenId = i32; pub type TokenId = i32;
pub type IssueStatusId = i32; pub type IssueStatusId = i32;
@ -468,10 +469,21 @@ pub struct User {
pub name: String, pub name: String,
pub email: String, pub email: String,
pub avatar_url: Option<String>, pub avatar_url: Option<String>,
pub project_id: ProjectId,
pub created_at: NaiveDateTime, pub created_at: NaiveDateTime,
pub updated_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))] #[cfg_attr(feature = "backend", derive(Queryable))]
@ -533,6 +545,7 @@ impl From<Issue> for UpdateIssuePayload {
} }
} }
#[cfg_attr(feature = "backend", derive(Queryable))]
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)] #[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
pub struct Message { pub struct Message {
pub id: MessageId, pub id: MessageId,
@ -656,6 +669,7 @@ pub enum IssueFieldId {
pub enum WsMsg { pub enum WsMsg {
Ping, Ping,
Pong, Pong,
Die,
// auth // auth
AuthorizeRequest(Uuid), AuthorizeRequest(Uuid),
@ -697,6 +711,9 @@ pub enum WsMsg {
// project page // project page
ProjectRequest, ProjectRequest,
ProjectLoaded(Project), ProjectLoaded(Project),
ProjectsLoad,
ProjectsLoaded(Vec<Project>),
ProjectIssuesRequest, ProjectIssuesRequest,
ProjectIssuesLoaded(Vec<Issue>), ProjectIssuesLoaded(Vec<Issue>),
ProjectUsersRequest, ProjectUsersRequest,
@ -734,6 +751,12 @@ pub enum WsMsg {
ProfileUpdate(EmailString, UsernameString), ProfileUpdate(EmailString, UsernameString),
ProfileUpdated, ProfileUpdated,
// user projects
UserProjectLoad,
UserProjectLoaded(Vec<UserProject>),
UserProjectSetCurrent(UserProjectId),
UserProjectCurrentChanged(UserProject),
// messages // messages
Message(Message), Message(Message),
MessagesRequest, MessagesRequest,

View File

@ -8,16 +8,6 @@ repository = "https://gitlab.com/adrian.wozniak/jirs"
license = "MPL-2.0" license = "MPL-2.0"
#license-file = "../LICENSE" #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]] [[bin]]
name = "jirs_server" name = "jirs_server"
path = "./src/main.rs" path = "./src/main.rs"

View File

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

View File

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

View File

@ -0,0 +1 @@
DROP TABLE IF EXISTS messages;

View File

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

View File

@ -3,23 +3,27 @@ insert into projects (name) values ('initial'), ('second'), ('third');
insert into issue_statuses (name, project_id, position) insert into issue_statuses (name, project_id, position)
values ('backlog', 1, 1), ('selected', 1, 2), ('in_progress', 1, 3), ('done', 1, 4); values ('backlog', 1, 1), ('selected', 1, 2), ('in_progress', 1, 3), ('done', 1, 4);
insert into users (project_id, email, name, avatar_url) values ( insert into users (email, name, avatar_url) values (
1,
'john@example.com', 'john@example.com',
'John Doe', 'John Doe',
'http://cdn.onlinewebfonts.com/svg/img_553934.png' 'http://cdn.onlinewebfonts.com/svg/img_553934.png'
), ( ), (
1,
'kate@exampe.com', 'kate@exampe.com',
'Kate Snow', 'Kate Snow',
'http://www.asthmamd.org/images/icon_user_6.png' 'http://www.asthmamd.org/images/icon_user_6.png'
), ( ), (
1,
'mike@example.com', 'mike@example.com',
'Mike Keningham', 'Mike Keningham',
'https://cdn0.iconfinder.com/data/icons/user-pictures/100/matureman1-512.png' '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@example.com',
'Foo1', 'Foo1',
'sent', 'sent',

View File

@ -8,7 +8,7 @@ use jirs_data::{
use crate::db::DbExecutor; use crate::db::DbExecutor;
use crate::errors::ServiceErrors; use crate::errors::ServiceErrors;
use crate::models::{InvitationForm, UserForm}; use crate::models::InvitationForm;
pub struct ListInvitation { pub struct ListInvitation {
pub user_id: UserId, pub user_id: UserId,
@ -150,7 +150,6 @@ impl Handler<AcceptInvitation> for DbExecutor {
fn handle(&mut self, msg: AcceptInvitation, _ctx: &mut Self::Context) -> Self::Result { fn handle(&mut self, msg: AcceptInvitation, _ctx: &mut Self::Context) -> Self::Result {
use crate::schema::invitations::dsl::*; use crate::schema::invitations::dsl::*;
use crate::schema::users::dsl::users;
let conn = &self let conn = &self
.pool .pool
@ -181,17 +180,26 @@ impl Handler<AcceptInvitation> for DbExecutor {
.execute(conn) .execute(conn)
.map_err(|e| ServiceErrors::DatabaseQueryFailed(format!("{}", e)))?; .map_err(|e| ServiceErrors::DatabaseQueryFailed(format!("{}", e)))?;
let form = UserForm { let user: User = {
name: invitation.name, use crate::schema::users::dsl::*;
email: invitation.email,
avatar_url: None, let query = diesel::insert_into(users)
project_id: invitation.project_id, .values((name.eq(invitation.name), email.eq(invitation.email)));
debug!("{}", diesel::debug_query::<Pg, _>(&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::<Pg, _>(&query));
query
.execute(conn)
.map_err(|e| ServiceErrors::DatabaseQueryFailed(format!("{}", e)))?;
}; };
let query = diesel::insert_into(users).values(form);
debug!("{}", diesel::debug_query::<Pg, _>(&query).to_string());
let user: User = query
.get_result(conn)
.map_err(|e| ServiceErrors::DatabaseQueryFailed(format!("{}", e)))?;
Ok(user) Ok(user)
} }

View File

@ -17,6 +17,7 @@ pub mod issue_statuses;
pub mod issues; pub mod issues;
pub mod projects; pub mod projects;
pub mod tokens; pub mod tokens;
pub mod user_projects;
pub mod users; pub mod users;
#[cfg(debug_assertions)] #[cfg(debug_assertions)]

View File

@ -3,10 +3,11 @@ use diesel::pg::Pg;
use diesel::prelude::*; use diesel::prelude::*;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use jirs_data::{Project, ProjectCategory, TimeTracking}; use jirs_data::{Project, ProjectCategory, TimeTracking, UserId};
use crate::db::DbExecutor; use crate::db::DbExecutor;
use crate::errors::ServiceErrors; use crate::errors::ServiceErrors;
use crate::schema::projects::all_columns;
#[derive(Serialize, Deserialize)] #[derive(Serialize, Deserialize)]
pub struct LoadCurrentProject { pub struct LoadCurrentProject {
@ -83,3 +84,34 @@ impl Handler<UpdateProject> for DbExecutor {
.map_err(|_| ServiceErrors::RecordNotFound("Project".to_string())) .map_err(|_| ServiceErrors::RecordNotFound("Project".to_string()))
} }
} }
pub struct LoadProjects {
pub user_id: UserId,
}
impl Message for LoadProjects {
type Result = Result<Vec<Project>, ServiceErrors>;
}
impl Handler<LoadProjects> for DbExecutor {
type Result = Result<Vec<Project>, 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::<diesel::pg::Pg, _>(&query));
query
.load::<Project>(conn)
.map_err(|_| ServiceErrors::RecordNotFound("Project".to_string()))
}
}

View File

@ -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<UserProject, ServiceErrors>;
}
impl Handler<CurrentUserProject> for DbExecutor {
type Result = Result<UserProject, ServiceErrors>;
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::<Pg, _>(&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<Vec<UserProject>, ServiceErrors>;
}
impl Handler<LoadUserProjects> for DbExecutor {
type Result = Result<Vec<UserProject>, 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::<Pg, _>(&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<UserProject, ServiceErrors>;
}
impl Handler<ChangeCurrentUserProject> for DbExecutor {
type Result = Result<UserProject, ServiceErrors>;
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::<Pg, _>(&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::<Pg, _>(&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::<Pg, _>(&query));
query
.execute(conn)
.map(|_| ())
.map_err(|_e| ServiceErrors::RecordNotFound(format!("user project {}", msg.user_id)))?;
user_project.is_current = true;
Ok(user_project)
}
}

View File

@ -7,7 +7,8 @@ use jirs_data::{Project, User, UserId};
use crate::db::{DbExecutor, DbPooledConn}; use crate::db::{DbExecutor, DbPooledConn};
use crate::errors::ServiceErrors; use crate::errors::ServiceErrors;
use crate::models::{CreateProjectForm, UserForm}; use crate::models::CreateProjectForm;
use crate::schema::users::all_columns;
#[derive(Serialize, Deserialize, Debug)] #[derive(Serialize, Deserialize, Debug)]
pub struct FindUser { pub struct FindUser {
@ -54,6 +55,7 @@ impl Handler<LoadProjectUsers> for DbExecutor {
type Result = Result<Vec<User>, ServiceErrors>; type Result = Result<Vec<User>, ServiceErrors>;
fn handle(&mut self, msg: LoadProjectUsers, _ctx: &mut Self::Context) -> Self::Result { 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::*; use crate::schema::users::dsl::*;
let conn = &self let conn = &self
@ -61,7 +63,11 @@ impl Handler<LoadProjectUsers> for DbExecutor {
.get() .get()
.map_err(|_| ServiceErrors::DatabaseConnectionLost)?; .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::<Pg, _>(&users_query)); debug!("{}", diesel::debug_query::<Pg, _>(&users_query));
users_query users_query
.load(conn) .load(conn)
@ -142,19 +148,31 @@ impl Handler<Register> for DbExecutor {
.get_result(conn) .get_result(conn)
.map_err(|_| ServiceErrors::RegisterCollision)?; .map_err(|_| ServiceErrors::RegisterCollision)?;
let form = UserForm { let user: User = {
name: msg.name, let insert_user_query =
email: msg.email, diesel::insert_into(users).values((name.eq(msg.name), email.eq(msg.email)));
avatar_url: None, debug!("{}", diesel::debug_query::<Pg, _>(&insert_user_query));
project_id: project.id, insert_user_query
.get_result(conn)
.map_err(|_| ServiceErrors::RegisterCollision)?
}; };
let insert_user_query = diesel::insert_into(users).values(form); {
debug!("{}", diesel::debug_query::<Pg, _>(&insert_user_query)); use crate::schema::user_projects::dsl::*;
match insert_user_query.execute(conn) { let insert_user_project_query = diesel::insert_into(user_projects).values((
Ok(_) => (), user_id.eq(user.id),
_ => return Err(ServiceErrors::RegisterCollision), project_id.eq(project.id),
}; is_current.eq(true),
is_default.eq(true),
));
debug!(
"{}",
diesel::debug_query::<Pg, _>(&insert_user_project_query)
);
insert_user_project_query
.execute(conn)
.map_err(|_| ServiceErrors::RegisterCollision)?;
}
Ok(()) Ok(())
} }
@ -287,11 +305,13 @@ mod tests {
#[test] #[test]
fn check_collision() { fn check_collision() {
use crate::schema::projects::dsl::projects; use crate::schema::projects::dsl::projects;
use crate::schema::user_projects::dsl::user_projects;
use crate::schema::users::dsl::users; use crate::schema::users::dsl::users;
let pool = build_pool(); let pool = build_pool();
let conn = &pool.get().unwrap(); let conn = &pool.get().unwrap();
diesel::delete(user_projects).execute(conn).unwrap();
diesel::delete(users).execute(conn).unwrap(); diesel::delete(users).execute(conn).unwrap();
diesel::delete(projects).execute(conn).unwrap(); diesel::delete(projects).execute(conn).unwrap();
@ -306,16 +326,28 @@ mod tests {
.get_result(conn) .get_result(conn)
.unwrap(); .unwrap();
let user_form = UserForm { let user: User = {
name: "Foo".to_string(), use crate::schema::users::dsl::*;
email: "foo@example.com".to_string(), diesel::insert_into(users)
avatar_url: None, .values((
project_id: project.id, name.eq("Foo".to_string()),
email.eq("foo@example.com".to_string()),
))
.get_result(conn)
.unwrap()
}; };
diesel::insert_into(users) {
.values(user_form) use crate::schema::user_projects::dsl::*;
.execute(conn) diesel::insert_into(user_projects)
.unwrap(); .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("Foo", "bar@example.com", conn), 1);
assert_eq!(count_matching_users("Bar", "foo@example.com", conn), 1); assert_eq!(count_matching_users("Bar", "foo@example.com", conn), 1);

View File

@ -109,7 +109,6 @@ pub struct UserForm {
pub name: String, pub name: String,
pub email: String, pub email: String,
pub avatar_url: Option<String>, pub avatar_url: Option<String>,
pub project_id: i32,
} }
#[derive(Debug, Serialize, Deserialize, Insertable)] #[derive(Debug, Serialize, Deserialize, Insertable)]

View File

@ -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! { table! {
use diesel::sql_types::*; use diesel::sql_types::*;
use jirs_data::sql::*; 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! { table! {
use diesel::sql_types::*; use diesel::sql_types::*;
use jirs_data::sql::*; use jirs_data::sql::*;
@ -445,12 +569,6 @@ table! {
/// ///
/// (Automatically generated by Diesel.) /// (Automatically generated by Diesel.)
avatar_url -> Nullable<Text>, avatar_url -> Nullable<Text>,
/// 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. /// The `created_at` column of the `users` table.
/// ///
/// Its SQL type is `Timestamp`. /// Its SQL type is `Timestamp`.
@ -463,12 +581,6 @@ table! {
/// ///
/// (Automatically generated by Diesel.) /// (Automatically generated by Diesel.)
updated_at -> Timestamp, 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 -> projects (project_id));
joinable!(issues -> users (reporter_id)); joinable!(issues -> users (reporter_id));
joinable!(tokens -> users (user_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!( allow_tables_to_appear_in_same_query!(
comments, comments,
@ -491,7 +604,9 @@ allow_tables_to_appear_in_same_query!(
issue_assignees, issue_assignees,
issues, issues,
issue_statuses, issue_statuses,
messages,
projects, projects,
tokens, tokens,
user_projects,
users, users,
); );

View File

@ -9,6 +9,7 @@ use actix_multipart::{Field, Multipart};
use actix_web::http::header::ContentDisposition; use actix_web::http::header::ContentDisposition;
use actix_web::web::Data; use actix_web::web::Data;
use actix_web::{post, web, Error, HttpResponse}; use actix_web::{post, web, Error, HttpResponse};
use futures::executor::block_on;
use futures::{StreamExt, TryStreamExt}; use futures::{StreamExt, TryStreamExt};
#[cfg(feature = "aws-s3")] #[cfg(feature = "aws-s3")]
use rusoto_s3::{PutObjectRequest, S3Client, S3}; use rusoto_s3::{PutObjectRequest, S3Client, S3};
@ -16,6 +17,7 @@ use rusoto_s3::{PutObjectRequest, S3Client, S3};
use jirs_data::{User, UserId, WsMsg}; use jirs_data::{User, UserId, WsMsg};
use crate::db::authorize_user::AuthorizeUser; use crate::db::authorize_user::AuthorizeUser;
use crate::db::user_projects::CurrentUserProject;
use crate::db::users::UpdateAvatarUrl; use crate::db::users::UpdateAvatarUrl;
use crate::db::DbExecutor; use crate::db::DbExecutor;
#[cfg(feature = "aws-s3")] #[cfg(feature = "aws-s3")]
@ -51,11 +53,21 @@ pub async fn upload(
_ => continue, _ => 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) { 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?; let user = update_user_avatar(user_id, avatar_url.clone(), db).await?;
ws.send(BroadcastToChannel( ws.send(BroadcastToChannel(
user.project_id, project_id,
WsMsg::AvatarUrlChanged(user.id, avatar_url), WsMsg::AvatarUrlChanged(user.id, avatar_url),
)) ))
.await .await

View File

@ -78,6 +78,9 @@ impl WsHandler<CheckAuthToken> for WebSocketActor {
_ => return Ok(Some(WsMsg::AuthorizeExpired)), _ => return Ok(Some(WsMsg::AuthorizeExpired)),
}; };
self.current_user = Some(user.clone()); 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())); block_on(self.join_channel(ctx.address().recipient()));
Ok(Some(WsMsg::AuthorizeLoaded(Ok(user)))) Ok(Some(WsMsg::AuthorizeLoaded(Ok(user))))
} }

View File

@ -16,7 +16,14 @@ impl WsHandler<LoadIssueComments> for WebSocketActor {
issue_id: msg.issue_id, issue_id: msg.issue_id,
})) { })) {
Ok(Ok(comments)) => comments, 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))) Ok(Some(WsMsg::IssueCommentsLoaded(comments)))
@ -38,7 +45,14 @@ impl WsHandler<CreateCommentPayload> for WebSocketActor {
body: msg.body, body: msg.body,
})) { })) {
Ok(Ok(_)) => (), 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) self.handle_msg(LoadIssueComments { issue_id }, ctx)
} }
@ -62,7 +76,14 @@ impl WsHandler<UpdateCommentPayload> for WebSocketActor {
body, body,
})) { })) {
Ok(Ok(comment)) => comment.issue_id, 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)? { if let Some(v) = self.handle_msg(LoadIssueComments { issue_id }, ctx)? {
self.broadcast(&v); self.broadcast(&v);
@ -87,7 +108,14 @@ impl WsHandler<DeleteComment> for WebSocketActor {
}; };
match block_on(self.db.send(m)) { match block_on(self.db.send(m)) {
Ok(Ok(_)) => (), 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))) Ok(Some(WsMsg::CommentDeleted(msg.comment_id)))

View File

@ -15,7 +15,14 @@ impl WsHandler<ListInvitation> for WebSocketActor {
}; };
let res = match block_on(self.db.send(invitations::ListInvitation { user_id })) { let res = match block_on(self.db.send(invitations::ListInvitation { user_id })) {
Ok(Ok(v)) => Some(WsMsg::InvitationListLoaded(v)), 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) Ok(res)
} }
@ -28,14 +35,15 @@ pub struct CreateInvitation {
impl WsHandler<CreateInvitation> for WebSocketActor { impl WsHandler<CreateInvitation> for WebSocketActor {
fn handle_msg(&mut self, msg: CreateInvitation, _ctx: &mut Self::Context) -> WsResult { fn handle_msg(&mut self, msg: CreateInvitation, _ctx: &mut Self::Context) -> WsResult {
let (user_id, inviter_name, project_id) = match self let project_id = match self.current_user_project.as_ref() {
.current_user Some(up) => up.project_id,
.as_ref()
.map(|u| (u.id, u.name.clone(), u.project_id))
{
Some(id) => id,
_ => return Ok(None), _ => 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 CreateInvitation { email, name } = msg;
let invitation = match block_on(self.db.send(invitations::CreateInvitation { let invitation = match block_on(self.db.send(invitations::CreateInvitation {
@ -84,7 +92,14 @@ impl WsHandler<DeleteInvitation> for WebSocketActor {
let DeleteInvitation { id } = msg; let DeleteInvitation { id } = msg;
let res = match block_on(self.db.send(invitations::DeleteInvitation { id })) { let res = match block_on(self.db.send(invitations::DeleteInvitation { id })) {
Ok(Ok(_)) => None, Ok(Ok(_)) => None,
_ => None, Ok(Err(e)) => {
error!("{:?}", e);
return Ok(None);
}
Err(e) => {
error!("{}", e);
return Ok(None);
}
}; };
Ok(res) Ok(res)
} }
@ -100,7 +115,14 @@ impl WsHandler<RevokeInvitation> for WebSocketActor {
let RevokeInvitation { id } = msg; let RevokeInvitation { id } = msg;
let res = match block_on(self.db.send(invitations::RevokeInvitation { id })) { let res = match block_on(self.db.send(invitations::RevokeInvitation { id })) {
Ok(Ok(_)) => Some(WsMsg::InvitationRevokeSuccess(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) Ok(res)
} }
@ -116,7 +138,14 @@ impl WsHandler<AcceptInvitation> for WebSocketActor {
let AcceptInvitation { id } = msg; let AcceptInvitation { id } = msg;
let res = match block_on(self.db.send(invitations::AcceptInvitation { id })) { let res = match block_on(self.db.send(invitations::AcceptInvitation { id })) {
Ok(Ok(_)) => Some(WsMsg::InvitationAcceptSuccess(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) Ok(res)
} }

View File

@ -9,14 +9,21 @@ pub struct LoadIssueStatuses;
impl WsHandler<LoadIssueStatuses> for WebSocketActor { impl WsHandler<LoadIssueStatuses> for WebSocketActor {
fn handle_msg(&mut self, _msg: LoadIssueStatuses, _ctx: &mut Self::Context) -> WsResult { 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( let msg = match block_on(
self.db self.db
.send(issue_statuses::LoadIssueStatuses { project_id }), .send(issue_statuses::LoadIssueStatuses { project_id }),
) { ) {
Ok(Ok(v)) => Some(WsMsg::IssueStatusesResponse(v)), 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) Ok(msg)
} }
@ -29,7 +36,7 @@ pub struct CreateIssueStatus {
impl WsHandler<CreateIssueStatus> for WebSocketActor { impl WsHandler<CreateIssueStatus> for WebSocketActor {
fn handle_msg(&mut self, msg: CreateIssueStatus, _ctx: &mut Self::Context) -> WsResult { 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 CreateIssueStatus { position, name } = msg;
let msg = match block_on(self.db.send(issue_statuses::CreateIssueStatus { let msg = match block_on(self.db.send(issue_statuses::CreateIssueStatus {
@ -38,7 +45,14 @@ impl WsHandler<CreateIssueStatus> for WebSocketActor {
name, name,
})) { })) {
Ok(Ok(is)) => Some(WsMsg::IssueStatusCreated(is)), 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) Ok(msg)
} }
@ -50,7 +64,7 @@ pub struct DeleteIssueStatus {
impl WsHandler<DeleteIssueStatus> for WebSocketActor { impl WsHandler<DeleteIssueStatus> for WebSocketActor {
fn handle_msg(&mut self, msg: DeleteIssueStatus, _ctx: &mut Self::Context) -> WsResult { 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 DeleteIssueStatus { issue_status_id } = msg;
let msg = match block_on(self.db.send(issue_statuses::DeleteIssueStatus { let msg = match block_on(self.db.send(issue_statuses::DeleteIssueStatus {
@ -58,7 +72,14 @@ impl WsHandler<DeleteIssueStatus> for WebSocketActor {
project_id, project_id,
})) { })) {
Ok(Ok(is)) => Some(WsMsg::IssueStatusDeleted(is)), 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) Ok(msg)
} }
@ -72,7 +93,7 @@ pub struct UpdateIssueStatus {
impl WsHandler<UpdateIssueStatus> for WebSocketActor { impl WsHandler<UpdateIssueStatus> for WebSocketActor {
fn handle_msg(&mut self, msg: UpdateIssueStatus, _ctx: &mut Self::Context) -> WsResult { 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 { let UpdateIssueStatus {
issue_status_id, issue_status_id,
@ -86,7 +107,14 @@ impl WsHandler<UpdateIssueStatus> for WebSocketActor {
project_id, project_id,
})) { })) {
Ok(Ok(is)) => Some(WsMsg::IssueStatusUpdated(is)), 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() { if let Some(ws_msg) = msg.as_ref() {
self.broadcast(ws_msg) self.broadcast(ws_msg)

View File

@ -71,7 +71,14 @@ impl WsHandler<UpdateIssueHandler> for WebSocketActor {
let assignees: Vec<IssueAssignee> = let assignees: Vec<IssueAssignee> =
match block_on(self.db.send(LoadAssignees { issue_id: issue.id })) { match block_on(self.db.send(LoadAssignees { issue_id: issue.id })) {
Ok(Ok(v)) => v, 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 { for assignee in assignees {
@ -102,7 +109,14 @@ impl WsHandler<CreateIssuePayload> for WebSocketActor {
}; };
let m = match block_on(self.db.send(msg)) { let m = match block_on(self.db.send(msg)) {
Ok(Ok(issue)) => Some(WsMsg::IssueCreated(issue.into())), 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) Ok(m)
} }
@ -120,7 +134,14 @@ impl WsHandler<DeleteIssue> for WebSocketActor {
.send(crate::db::issues::DeleteIssue { issue_id: msg.id }), .send(crate::db::issues::DeleteIssue { issue_id: msg.id }),
) { ) {
Ok(Ok(_)) => Some(WsMsg::IssueDeleted(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) Ok(m)
} }
@ -130,7 +151,7 @@ pub struct LoadIssues;
impl WsHandler<LoadIssues> for WebSocketActor { impl WsHandler<LoadIssues> for WebSocketActor {
fn handle_msg(&mut self, _msg: LoadIssues, _ctx: &mut Self::Context) -> WsResult { 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<jirs_data::Issue> = let issues: Vec<jirs_data::Issue> =
match block_on(self.db.send(LoadProjectIssues { project_id })) { match block_on(self.db.send(LoadProjectIssues { project_id })) {

View File

@ -6,9 +6,12 @@ use actix::{
use actix_web::web::Data; use actix_web::web::Data;
use actix_web::{get, web, Error, HttpRequest, HttpResponse}; use actix_web::{get, web, Error, HttpRequest, HttpResponse};
use actix_web_actors::ws; 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::db::DbExecutor;
use crate::mail::MailExecutor; use crate::mail::MailExecutor;
use crate::ws::auth::*; use crate::ws::auth::*;
@ -25,6 +28,7 @@ pub mod invitations;
pub mod issue_statuses; pub mod issue_statuses;
pub mod issues; pub mod issues;
pub mod projects; pub mod projects;
pub mod user_projects;
pub mod users; pub mod users;
pub type WsResult = std::result::Result<Option<WsMsg>, WsMsg>; pub type WsResult = std::result::Result<Option<WsMsg>, WsMsg>;
@ -36,8 +40,10 @@ trait WsMessageSender {
struct WebSocketActor { struct WebSocketActor {
db: Data<Addr<DbExecutor>>, db: Data<Addr<DbExecutor>>,
mail: Data<Addr<MailExecutor>>, mail: Data<Addr<MailExecutor>>,
current_user: Option<jirs_data::User>,
addr: Addr<WsServer>, addr: Addr<WsServer>,
current_user: Option<jirs_data::User>,
current_user_project: Option<jirs_data::UserProject>,
current_project: Option<jirs_data::Project>,
} }
impl Actor for WebSocketActor { impl Actor for WebSocketActor {
@ -62,12 +68,12 @@ impl Handler<InnerMsg> for WebSocketActor {
impl WebSocketActor { impl WebSocketActor {
fn broadcast(&self, msg: &WsMsg) { fn broadcast(&self, msg: &WsMsg) {
let user = match self.current_user.as_ref() { let project_id = match self.require_user_project() {
Some(u) => u, Ok(up) => up.project_id,
_ => return, _ => return,
}; };
self.addr self.addr
.do_send(InnerMsg::BroadcastToChannel(user.project_id, msg.clone())); .do_send(InnerMsg::BroadcastToChannel(project_id, msg.clone()));
} }
fn handle_ws_msg( fn handle_ws_msg(
@ -179,13 +185,18 @@ impl WebSocketActor {
async fn join_channel(&self, addr: Recipient<InnerMsg>) { async fn join_channel(&self, addr: Recipient<InnerMsg>) {
info!("joining channel..."); info!("joining channel...");
info!(" current user {:?}", self.current_user); info!(" current user {:?}", self.current_user);
let user = match self.current_user.as_ref() { let user = match self.current_user.as_ref() {
None => return, None => return,
Some(u) => u, Some(u) => u,
}; };
let project_id = match self.require_user_project() {
Ok(user_project) => user_project.project_id,
_ => return,
};
match self match self
.addr .addr
.send(InnerMsg::Join(user.project_id, user.id, addr)) .send(InnerMsg::Join(project_id, user.id, addr))
.await .await
{ {
Err(e) => error!("{}", e), 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 self.current_user
.as_ref() .as_ref()
.map(|u| u) .map(|u| u)
.ok_or_else(|| WsMsg::AuthorizeExpired) .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<UserProject, WsMsg> {
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<Project, WsMsg> {
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<Result<ws::Message, ws::ProtocolError>> for WebSocketActor { impl StreamHandler<Result<ws::Message, ws::ProtocolError>> for WebSocketActor {
@ -226,9 +267,12 @@ impl StreamHandler<Result<ws::Message, ws::ProtocolError>> for WebSocketActor {
fn finished(&mut self, ctx: &mut Self::Context) { fn finished(&mut self, ctx: &mut Self::Context) {
info!("Disconnected"); 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( self.addr.do_send(InnerMsg::Leave(
user.project_id, up.project_id,
user.id, user.id,
ctx.address().recipient(), ctx.address().recipient(),
)); ));
@ -353,6 +397,8 @@ pub async fn index(
db, db,
mail, mail,
current_user: None, current_user: None,
current_user_project: None,
current_project: None,
addr: ws_server.get_ref().clone(), addr: ws_server.get_ref().clone(),
}, },
&req, &req,

View File

@ -9,7 +9,7 @@ pub struct CurrentProject;
impl WsHandler<CurrentProject> for WebSocketActor { impl WsHandler<CurrentProject> for WebSocketActor {
fn handle_msg(&mut self, _msg: CurrentProject, _ctx: &mut Self::Context) -> WsResult { 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 })) { let m = match block_on(self.db.send(LoadCurrentProject { project_id })) {
Ok(Ok(project)) => Some(WsMsg::ProjectLoaded(project)), Ok(Ok(project)) => Some(WsMsg::ProjectLoaded(project)),
@ -28,7 +28,7 @@ impl WsHandler<CurrentProject> for WebSocketActor {
impl WsHandler<UpdateProjectPayload> for WebSocketActor { impl WsHandler<UpdateProjectPayload> for WebSocketActor {
fn handle_msg(&mut self, msg: UpdateProjectPayload, _ctx: &mut Self::Context) -> WsResult { 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 { let project = match block_on(self.db.send(crate::db::projects::UpdateProject {
project_id, project_id,
name: msg.name, name: msg.name,

View File

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

View File

@ -12,10 +12,17 @@ impl WsHandler<LoadProjectUsers> for WebSocketActor {
fn handle_msg(&mut self, _msg: LoadProjectUsers, _ctx: &mut Self::Context) -> WsResult { fn handle_msg(&mut self, _msg: LoadProjectUsers, _ctx: &mut Self::Context) -> WsResult {
use crate::db::users::LoadProjectUsers as Msg; 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 })) { let m = match block_on(self.db.send(Msg { project_id })) {
Ok(Ok(v)) => Some(WsMsg::ProjectUsersLoaded(v)), 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) Ok(m)
} }
@ -35,7 +42,10 @@ impl WsHandler<Register> for WebSocketActor {
})) { })) {
Ok(Ok(_)) => Some(WsMsg::SignUpSuccess), Ok(Ok(_)) => Some(WsMsg::SignUpSuccess),
Ok(Err(_)) => Some(WsMsg::SignUpPairTaken), Ok(Err(_)) => Some(WsMsg::SignUpPairTaken),
_ => None, Err(e) => {
error!("{}", e);
return Ok(None);
}
}; };
match self.handle_msg(Authenticate { name, email }, ctx) { match self.handle_msg(Authenticate { name, email }, ctx) {
@ -78,7 +88,14 @@ impl WsHandler<ProfileUpdate> for WebSocketActor {
email, email,
})) { })) {
Ok(Ok(_users)) => (), 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)) Ok(Some(WsMsg::ProfileUpdated))