Add more verbose errors. Fix register

This commit is contained in:
Adrian Woźniak 2020-10-21 23:59:17 +02:00
parent 8e939faebd
commit d3f30e03a1
36 changed files with 1395 additions and 1056 deletions

12
Cargo.lock generated
View File

@ -3116,9 +3116,9 @@ checksum = "388a1df253eca08550bef6c72392cfe7c30914bf41df5269b68cbd6ff8f570a3"
[[package]] [[package]]
name = "serde" name = "serde"
version = "1.0.116" version = "1.0.117"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "96fe57af81d28386a513cbc6858332abc6117cfdb5999647c6444b8f43a370a5" checksum = "b88fa983de7720629c9387e9f517353ed404164b1e482c970a90c1a4aaf7dc1a"
dependencies = [ dependencies = [
"serde_derive", "serde_derive",
] ]
@ -3135,9 +3135,9 @@ dependencies = [
[[package]] [[package]]
name = "serde_derive" name = "serde_derive"
version = "1.0.116" version = "1.0.117"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f630a6370fd8e457873b4bd2ffdae75408bc291ba72be773772a4c2a065d9ae8" checksum = "cbd1ae72adb44aab48f325a02444a5fc079349a8d804c1fc922aed3f7454c74e"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
@ -3329,9 +3329,9 @@ checksum = "2d67a5a62ba6e01cb2192ff309324cb4875d0c451d55fe2319433abe7a05a8ee"
[[package]] [[package]]
name = "syn" name = "syn"
version = "1.0.44" version = "1.0.45"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e03e57e4fcbfe7749842d53e24ccb9aa12b7252dbe5e91d2acad31834c8b8fdd" checksum = "ea9c5432ff16d6152371f808fb5a871cd67368171b09bb21b43df8e4a47a3556"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",

15
Dockerfile.build Normal file
View File

@ -0,0 +1,15 @@
FROM ubuntu:18.04
WORKDIR /app/
RUN apt-get update && apt-get install -y curl git openssl libpq-dev gcc openssl1.0 make cmake
RUN curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- --default-toolchain nightly -y
RUN . $HOME/.cargo/env && \
rustup toolchain install nightly && rustup default nightly
RUN ls -al /app
CMD . $HOME/.cargo/env && \
cd ./jirs-server && \
rm -Rf ./target/debug/jirs_server && \
cargo build --bin jirs_server --release --no-default-features --features local-storage && \
cp /app/target/release/jirs_server /app/build/

View File

@ -7,6 +7,24 @@ services:
- POSTGRES_USER=postgres - POSTGRES_USER=postgres
- POSTGRES_HOST_AUTH_METHOD=trust - POSTGRES_HOST_AUTH_METHOD=trust
build-server:
build:
dockerfile: ./Dockerfile.build
context: .
stdin_open: true
tty: true
volumes:
- ./build:/app/build
- type: volume
source: rs-target
target: /app/jirs-server/target
- ./Cargo.toml:/app/Cargo.toml
- ./Cargo.lock:/app/Cargo.lock
- ./jirs-server:/app/jirs-server
- ./jirs-data:/app/jirs-data
- ./jirs-cli:/app/jirs-cli
- ./jirs-client:/app/jirs-client
- ./jirs-css:/app/jirs-css
server: server:
build: build:
dockerfile: ./jirs-server/Dockerfile dockerfile: ./jirs-server/Dockerfile
@ -57,3 +75,5 @@ services:
volumes: volumes:
assets: assets:
build:
rs-target:

View File

@ -29,7 +29,7 @@ lto = true
opt-level = 's' opt-level = 's'
[dependencies] [dependencies]
jirs-data = { path = "../jirs-data" } jirs-data = { path = "../jirs-data", features = ["frontend"] }
wee_alloc = "*" wee_alloc = "*"

View File

@ -1,6 +1,6 @@
use syntect::easy::HighlightLines; // use syntect::easy::HighlightLines;
use syntect::highlighting::{FontStyle, Style}; // use syntect::highlighting::{FontStyle, Style};
use syntect::parsing::SyntaxReference; // use syntect::parsing::SyntaxReference;
use wasm_bindgen::prelude::*; use wasm_bindgen::prelude::*;
#[wasm_bindgen(final, js_name = JirsCodeBuilder)] #[wasm_bindgen(final, js_name = JirsCodeBuilder)]
@ -15,51 +15,52 @@ impl JirsCodeBuilder {
} }
#[wasm_bindgen] #[wasm_bindgen]
pub fn hi_code(&mut self, lang: &str, code: &str) -> String { pub fn hi_code(&mut self, _lang: &str, code: &str) -> String {
let syntax = { // let syntax = {
match crate::hi::syntax_set::load().find_syntax_by_name(lang) { // match crate::hi::syntax_set::load().find_syntax_by_name(lang) {
Some(s) => s.clone(), // Some(s) => s.clone(),
_ => { // _ => {
return code.to_string(); // return code.to_string();
} // }
} // }
}; // };
let mut buffer: Vec<String> = Vec::with_capacity(code.lines().count() * 2); // let mut buffer: Vec<String> = Vec::with_capacity(code.lines().count() * 2);
for line in code.lines() { // for line in code.lines() {
self.hi(&syntax, line, &mut buffer); // self.hi(&syntax, line, &mut buffer);
buffer.push("<br />".to_string()); // buffer.push("<br />".to_string());
} // }
buffer.join("") // buffer.join("")
code.to_string()
} }
fn hi<'l>(&mut self, syntax: &SyntaxReference, line: &'l str, buffer: &mut Vec<String>) { // fn hi<'l>(&mut self, syntax: &SyntaxReference, line: &'l str, buffer: &mut Vec<String>) {
let mut h = HighlightLines::new(syntax, &crate::hi::THEME_SET.themes["base16-ocean-dark"]); // inspired-github // let mut h = HighlightLines::new(syntax, &crate::hi::THEME_SET.themes["base16-ocean-dark"]); // inspired-github
let tokens = { h.highlight(line, &crate::hi::syntax_set::load()) }; // let tokens = { h.highlight(line, &crate::hi::syntax_set::load()) };
//
for (style, token) in tokens.into_iter() { // for (style, token) in tokens.into_iter() {
let Style { // let Style {
foreground: f, // foreground: f,
background: b, // background: b,
font_style, // font_style,
} = style; // } = style;
let fs = if font_style == FontStyle::BOLD { // let fs = if font_style == FontStyle::BOLD {
"font-weight: bold" // "font-weight: bold"
} else if font_style == FontStyle::ITALIC { // } else if font_style == FontStyle::ITALIC {
"font-style: italic" // "font-style: italic"
} else if font_style == FontStyle::UNDERLINE { // } else if font_style == FontStyle::UNDERLINE {
"text-decoration: underline" // "text-decoration: underline"
} else { // } else {
"" // ""
}; // };
//
buffer.push(format!( // buffer.push(format!(
r#"<span style="color: rgba({f_r}, {f_g}, {f_b}, {f_a});background:rgba({b_r}, {b_g}, {b_b}, {b_a}); {fs}">{t}</span>"#, // r#"<span style="color: rgba({f_r}, {f_g}, {f_b}, {f_a});background:rgba({b_r}, {b_g}, {b_b}, {b_a}); {fs}">{t}</span>"#,
t = if token.is_empty() { "&nbsp;" } else { token }, // t = if token.is_empty() { "&nbsp;" } else { token },
f_r = f.r, f_g = f.g, f_b = f.b, f_a = f.a, b_r = b.r, b_g = b.g, b_b = b.b, b_a = b.a, // f_r = f.r, f_g = f.g, f_b = f.b, f_a = f.a, b_r = b.r, b_g = b.g, b_b = b.b, b_a = b.a,
fs = fs // fs = fs
)); // ));
} // }
} // }
} }
pub fn define() { pub fn define() {

View File

@ -9,10 +9,10 @@ use jirs_data::*;
use crate::model::{ModalType, Model, Page}; use crate::model::{ModalType, Model, Page};
use crate::shared::styled_date_time_input::StyledDateTimeChanged; use crate::shared::styled_date_time_input::StyledDateTimeChanged;
use crate::shared::styled_rte::RteMsg; use crate::shared::{go_to_board, go_to_login, styled_tooltip};
// use crate::shared::styled_rte::RteMsg;
use crate::shared::styled_select::StyledSelectChanged; use crate::shared::styled_select::StyledSelectChanged;
use crate::shared::styled_tooltip::{Variant as StyledTooltip, Variant}; use crate::shared::styled_tooltip::{Variant as StyledTooltip, Variant};
use crate::shared::{go_to_board, go_to_login, styled_tooltip};
use crate::ws::{flush_queue, open_socket, read_incoming, send_ws_msg}; use crate::ws::{flush_queue, open_socket, read_incoming, send_ws_msg};
mod changes; mod changes;
@ -85,7 +85,7 @@ pub enum Msg {
StrInputChanged(FieldId, String), StrInputChanged(FieldId, String),
U32InputChanged(FieldId, u32), U32InputChanged(FieldId, u32),
FileInputChanged(FieldId, Vec<File>), FileInputChanged(FieldId, Vec<File>),
Rte(FieldId, RteMsg), // Rte(FieldId, RteMsg),
// issues // issues
AddIssue, AddIssue,
@ -279,9 +279,9 @@ pub fn render(host_url: String, ws_url: String) {
HOST_URL = host_url; HOST_URL = host_url;
WS_URL = ws_url; WS_URL = ws_url;
} }
if !cfg!(debug_assertions) { // if !cfg!(debug_assertions) {
crate::hi::syntax_set::load(); // crate::hi::syntax_set::load();
} // }
elements::define(); elements::define();
let _app = seed::App::builder(update, view) let _app = seed::App::builder(update, view)

View File

@ -14,7 +14,7 @@ use crate::{
drag::DragState, styled_checkbox::StyledCheckboxState, drag::DragState, styled_checkbox::StyledCheckboxState,
styled_date_time_input::StyledDateTimeInputState, styled_editor::Mode, styled_date_time_input::StyledDateTimeInputState, styled_editor::Mode,
styled_image_input::StyledImageInputState, styled_input::StyledInputState, styled_image_input::StyledImageInputState, styled_input::StyledInputState,
styled_rte::StyledRteState, styled_select::StyledSelectState, /*styled_rte::StyledRteState,*/ styled_select::StyledSelectState,
}, },
EditIssueModalSection, FieldId, Msg, ProjectFieldId, EditIssueModalSection, FieldId, Msg, ProjectFieldId,
}; };
@ -371,7 +371,7 @@ pub struct ProjectSettingsPage {
pub edit_column_id: Option<IssueStatusId>, pub edit_column_id: Option<IssueStatusId>,
pub creating_issue_status: bool, pub creating_issue_status: bool,
pub name: StyledInputState, pub name: StyledInputState,
pub description_rte: StyledRteState, // pub description_rte: StyledRteState,
} }
impl ProjectSettingsPage { impl ProjectSettingsPage {
@ -411,9 +411,9 @@ impl ProjectSettingsPage {
FieldId::ProjectSettings(ProjectFieldId::IssueStatusName), FieldId::ProjectSettings(ProjectFieldId::IssueStatusName),
"", "",
), ),
description_rte: StyledRteState::new(FieldId::ProjectSettings( // description_rte: StyledRteState::new(FieldId::ProjectSettings(
ProjectFieldId::Description, // ProjectFieldId::Description,
)), // )),
} }
} }

View File

@ -54,7 +54,7 @@ pub fn update(msg: Msg, model: &mut Model, orders: &mut impl Orders<Msg>) {
page.project_category_state.update(&msg, orders); page.project_category_state.update(&msg, orders);
page.time_tracking.update(&msg); page.time_tracking.update(&msg);
page.name.update(&msg); page.name.update(&msg);
page.description_rte.update(&msg, orders); // page.description_rte.update(&msg, orders);
match msg { match msg {
Msg::StrInputChanged(FieldId::ProjectSettings(ProjectFieldId::Name), text) => { Msg::StrInputChanged(FieldId::ProjectSettings(ProjectFieldId::Name), text) => {

View File

@ -12,11 +12,11 @@ use crate::shared::styled_field::StyledField;
use crate::shared::styled_form::StyledForm; use crate::shared::styled_form::StyledForm;
use crate::shared::styled_icon::{Icon, StyledIcon}; use crate::shared::styled_icon::{Icon, StyledIcon};
use crate::shared::styled_input::StyledInput; use crate::shared::styled_input::StyledInput;
use crate::shared::styled_rte::StyledRte;
use crate::shared::styled_select::StyledSelect;
use crate::shared::styled_textarea::StyledTextarea;
use crate::shared::{inner_layout, ToChild, ToNode}; use crate::shared::{inner_layout, ToChild, ToNode};
use crate::{model, FieldId, Msg, PageChanged, ProjectFieldId, ProjectPageChange}; use crate::{model, FieldId, Msg, PageChanged, ProjectFieldId, ProjectPageChange};
// use crate::shared::styled_rte::StyledRte;
use crate::shared::styled_select::StyledSelect;
use crate::shared::styled_textarea::StyledTextarea;
static TIME_TRACKING_FIBONACCI: &str = include_str!("./time_tracking_fibonacci.txt"); static TIME_TRACKING_FIBONACCI: &str = include_str!("./time_tracking_fibonacci.txt");
static TIME_TRACKING_HOURLY: &str = include_str!("./time_tracking_hourly.txt"); static TIME_TRACKING_HOURLY: &str = include_str!("./time_tracking_hourly.txt");
@ -32,15 +32,15 @@ pub fn view(model: &model::Model) -> Node<Msg> {
let description_field = description_field(page); let description_field = description_field(page);
let desc_rte = StyledField::build() // let desc_rte = StyledField::build()
.input( // .input(
StyledRte::build(FieldId::ProjectSettings(ProjectFieldId::Description)) // StyledRte::build(FieldId::ProjectSettings(ProjectFieldId::Description))
.state(&page.description_rte) // .state(&page.description_rte)
.build() // .build()
.into_node(), // .into_node(),
) // )
.build() // .build()
.into_node(); // .into_node();
let category_field = category_field(page); let category_field = category_field(page);
@ -89,7 +89,7 @@ pub fn view(model: &model::Model) -> Node<Msg> {
})) }))
.add_field(name_field) .add_field(name_field)
.add_field(url_field) .add_field(url_field)
.add_field(desc_rte) // .add_field(desc_rte)
.add_field(description_field) .add_field(description_field)
.add_field(category_field) .add_field(category_field)
.add_field(time_tracking_field) .add_field(time_tracking_field)

View File

@ -24,7 +24,7 @@ pub mod styled_image_input;
pub mod styled_input; pub mod styled_input;
pub mod styled_link; pub mod styled_link;
pub mod styled_modal; pub mod styled_modal;
pub mod styled_rte; // pub mod styled_rte;
pub mod styled_select; pub mod styled_select;
pub mod styled_select_child; pub mod styled_select_child;
pub mod styled_textarea; pub mod styled_textarea;

View File

@ -27,26 +27,13 @@ impl IntoNavItemIcon for Icon {
} }
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>) {
match msg { let m = match msg {
Msg::MessageInvitationApproved(token) => { Msg::MessageInvitationApproved(token) => WsMsg::InvitationAcceptRequest(*token),
send_ws_msg( Msg::MessageInvitationDismiss(token) => WsMsg::InvitationRejectRequest(*token),
WsMsg::InvitationAcceptRequest(*token), Msg::MessageSeen(id) => WsMsg::MessageMarkSeen(*id),
model.ws.as_ref(), _ => return,
orders, };
); send_ws_msg(m, model.ws.as_ref(), orders);
}
Msg::MessageInvitationDismiss(token) => {
send_ws_msg(
WsMsg::InvitationRejectRequest(*token),
model.ws.as_ref(),
orders,
);
}
Msg::MessageSeen(id) => {
send_ws_msg(WsMsg::MessageMarkSeen(*id), model.ws.as_ref(), orders);
}
_ => (),
}
} }
pub fn render(model: &Model) -> Vec<Node<Msg>> { pub fn render(model: &Model) -> Vec<Node<Msg>> {
@ -82,6 +69,15 @@ pub fn render(model: &Model) -> Vec<Node<Msg>> {
) )
}; };
let issue_nav = if model.issue_statuses.is_empty() {
vec![]
} else {
vec![
navbar_left_item("Search issues", Icon::Search, None, None),
navbar_left_item("Create Issue", Icon::Plus, Some("/add-issue"), None),
]
};
vec![ vec![
about_tooltip_popup(model), about_tooltip_popup(model),
messages_tooltip_popup(model), messages_tooltip_popup(model),
@ -92,8 +88,7 @@ pub fn render(model: &Model) -> Vec<Node<Msg>> {
attrs![At::Href => "/"], attrs![At::Href => "/"],
div![class!["styledLogo"], logo_svg] div![class!["styledLogo"], logo_svg]
], ],
navbar_left_item("Search issues", Icon::Search, None, None), issue_nav,
navbar_left_item("Create Issue", Icon::Plus, Some("/add-issue"), None),
div![ div![
class!["bottom"], class!["bottom"],
navbar_left_item("Profile", user_icon, Some("/profile"), None), navbar_left_item("Profile", user_icon, Some("/profile"), None),
@ -158,14 +153,11 @@ fn messages_tooltip_popup(model: &Model) -> Node<Msg> {
fn message_ui(model: &Model, message: &Message) -> Option<Node<Msg>> { fn message_ui(model: &Model, message: &Message) -> Option<Node<Msg>> {
let Message { let Message {
id, id,
receiver_id: _,
sender_id: _,
summary, summary,
description, description,
message_type, message_type,
hyper_link, hyper_link,
created_at: _, ..
updated_at: _,
} = message; } = message;
let message_id = *id; let message_id = *id;
@ -203,10 +195,7 @@ fn message_ui(model: &Model, message: &Message) -> Option<Node<Msg>> {
let node = match message_type { let node = match message_type {
MessageType::ReceivedInvitation => { MessageType::ReceivedInvitation => {
let token: InvitationToken = match hyper_link.trim_start_matches('#').parse() { let token: InvitationToken = hyper_link.trim_start_matches('#').parse().ok()?;
Err(_) => return None,
Ok(n) => n,
};
let accept = StyledButton::build() let accept = StyledButton::build()
.primary() .primary()
.text("Accept") .text("Accept")

View File

@ -70,10 +70,10 @@ impl<'l> StyledButtonBuilder<'l> {
self.variant(Variant::Empty) self.variant(Variant::Empty)
} }
pub fn button_id(mut self, button_id: ButtonId) -> Self { // pub fn button_id(mut self, button_id: ButtonId) -> Self {
self.button_id = Some(button_id); // self.button_id = Some(button_id);
self // self
} // }
pub fn disabled(mut self, value: bool) -> Self { pub fn disabled(mut self, value: bool) -> Self {
self.disabled = Some(value); self.disabled = Some(value);

View File

@ -173,13 +173,13 @@ impl<'l> StyledSelectBuilder<'l> {
} }
} }
pub fn try_state<'state: 'l>(self, state: Option<&'state StyledSelectState>) -> Self { // pub fn try_state<'state: 'l>(self, state: Option<&'state StyledSelectState>) -> Self {
if let Some(s) = state { // if let Some(s) = state {
self.state(s) // self.state(s)
} else { // } else {
self // self
} // }
} // }
pub fn state<'state: 'l>(self, state: &'state StyledSelectState) -> Self { pub fn state<'state: 'l>(self, state: &'state StyledSelectState) -> Self {
self.opened(state.opened) self.opened(state.opened)

View File

@ -89,15 +89,15 @@ impl<'l> StyledTooltipBuilder<'l> {
self self
} }
pub fn table_tooltip(mut self) -> Self { // pub fn table_tooltip(mut self) -> Self {
self.variant = Variant::TableBuilder; // self.variant = Variant::TableBuilder;
self // self
} // }
//
pub fn code_tooltip(mut self) -> Self { // pub fn code_tooltip(mut self) -> Self {
self.variant = Variant::CodeBuilder; // self.variant = Variant::CodeBuilder;
self // self
} // }
pub fn date_time_picker(mut self) -> Self { pub fn date_time_picker(mut self) -> Self {
self.variant = Variant::DateTimeBuilder; self.variant = Variant::DateTimeBuilder;

View File

@ -13,7 +13,8 @@ name = "jirs_data"
path = "./src/lib.rs" path = "./src/lib.rs"
[features] [features]
backend = [ "diesel" ] backend = ["diesel"]
frontend = []
[dependencies] [dependencies]
serde = "*" serde = "*"

View File

@ -14,7 +14,7 @@ pub use payloads::*;
pub use sql::*; pub use sql::*;
mod fields; mod fields;
mod msg; pub mod msg;
mod payloads; mod payloads;
#[cfg(feature = "backend")] #[cfg(feature = "backend")]
@ -80,6 +80,7 @@ impl IssueType {
} }
} }
#[cfg(feature = "frontend")]
impl Into<u32> for IssueType { impl Into<u32> for IssueType {
fn into(self) -> u32 { fn into(self) -> u32 {
match self { match self {
@ -90,6 +91,7 @@ impl Into<u32> for IssueType {
} }
} }
#[cfg(feature = "frontend")]
impl Into<IssueType> for u32 { impl Into<IssueType> for u32 {
fn into(self) -> IssueType { fn into(self) -> IssueType {
match self { match self {
@ -102,7 +104,7 @@ impl Into<IssueType> for u32 {
} }
impl IssueType { impl IssueType {
pub fn to_str(&self) -> &'static str { pub fn to_str<'l>(&self) -> &'l str {
match self { match self {
IssueType::Task => "task", IssueType::Task => "task",
IssueType::Bug => "bug", IssueType::Bug => "bug",

View File

@ -9,6 +9,114 @@ use crate::{
UsernameString, UsernameString,
}; };
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)]
#[repr(C)]
pub enum WsError {
InvalidLoginPair,
InvalidSignInToken,
// Issue status
NoIssueStatuses,
FailedToFetchIssueStatuses,
// tokens
FailedToDisableBindToken,
BindTokenNotExists,
NoBindToken,
FailedToCreateBindToken,
AccessTokenNotExists,
// users
UserNotExists(UserId),
NoMatchingPair(UsernameString, EmailString),
InvalidPair(UsernameString, EmailString),
TakenPair(UsernameString, EmailString),
FailedToLoadProjectUsers,
FailedToLoadAssignees,
FailedToChangeAvatar,
FailedToLoadInvitedUsers,
// user projects
InvalidUserProject,
// comments
FailedToLoadComments,
InvalidComment,
FailedToUpdateComment,
UnableToDeleteComment,
// epics
FailedToLoadEpics,
InvalidEpic,
FailedToUpdateEpic,
UnableToDeleteEpic,
// invitations
FailedToLoadInvitations,
InvalidInvitation,
FailedToUpdateInvitation,
UnableToDeleteInvitation,
InvitationRevoked,
}
impl WsError {
pub fn to_str<'l>(&self) -> &'l str {
match self {
WsError::InvalidLoginPair => "E-Mail and Login does not match",
WsError::InvalidSignInToken => "Given token is not valid",
WsError::NoIssueStatuses => {
"Failed to fetch first issue status. Are you sure there is any?"
}
WsError::FailedToFetchIssueStatuses => "Failed to load issue statuses",
WsError::FailedToDisableBindToken => "Failed to disable one use token",
WsError::BindTokenNotExists => "Used single use bind token does not exists in database",
WsError::NoBindToken => "Current user does not have any active tokens",
WsError::FailedToCreateBindToken => {
"Something went wrong when creating bind token. Please try later"
}
WsError::AccessTokenNotExists => "Token used for authentication does not exists",
WsError::UserNotExists(_) => "User does not exists",
WsError::NoMatchingPair(_, _) => "User for given pair does not exists",
WsError::FailedToLoadProjectUsers => {
"There was problem while loading project users. Please try later"
}
WsError::FailedToLoadAssignees => {
"There was problem while loading issue assignees. Please try later"
}
WsError::InvalidPair(_, _) => "Given sign up pair is not valid.",
WsError::TakenPair(_, _) => "Given sign up pair is already taken.",
WsError::InvalidUserProject => "Unable to connect user to project",
WsError::FailedToChangeAvatar => "Unable to change user avatar",
WsError::FailedToLoadInvitedUsers => "Failed to load invited users. Please try later",
// comments
WsError::FailedToLoadComments => "Failed to load comments. Please try later",
WsError::InvalidComment => "There is something wrong with given comment data",
WsError::FailedToUpdateComment => {
"There was problem when updating comment. Please try later"
}
WsError::UnableToDeleteComment => "Unable to delete comment",
// epics
WsError::FailedToLoadEpics => "Failed to load epics. Please try later",
WsError::InvalidEpic => "There is something wrong with given epic data",
WsError::FailedToUpdateEpic => {
"There was problem when updating comment. Please try later"
}
WsError::UnableToDeleteEpic => "Unable to delete epic",
// invitations
WsError::InvalidInvitation => "Given invitation contains problems",
WsError::FailedToLoadInvitations => "Failed to load invitations. Please try later",
WsError::FailedToUpdateInvitation => {
"There was problem when updating invitation. Please try later"
}
WsError::UnableToDeleteInvitation => "Unable to delete invitation",
WsError::InvitationRevoked => "This invitation is no longer valid",
}
}
}
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)] #[derive(Serialize, Deserialize, Debug, Clone, PartialEq)]
pub enum WsMsg { pub enum WsMsg {
Ping, Ping,
@ -124,4 +232,7 @@ pub enum WsMsg {
EpicUpdated(Epic), EpicUpdated(Epic),
EpicDelete(EpicId), EpicDelete(EpicId),
EpicDeleted(EpicId), EpicDeleted(EpicId),
// errors
Error(WsError),
} }

View File

@ -41,7 +41,6 @@ dotenv = { version = "*" }
byteorder = "1.0" byteorder = "1.0"
chrono = { version = "0.4", features = ["serde"] } chrono = { version = "0.4", features = ["serde"] }
libc = { version = "0.2.0" }
time = { version = "0.1" } time = { version = "0.1" }
url = { version = "2.1.0" } url = { version = "2.1.0" }
percent-encoding = { version = "2.1.0" } percent-encoding = { version = "2.1.0" }
@ -66,6 +65,7 @@ async-trait = { version = "*" }
futures = { version = "*" } futures = { version = "*" }
openssl-sys = { version = "*", features = ["vendored"] } openssl-sys = { version = "*", features = ["vendored"] }
libc = { version = "0.2.0", default-features = false }
lettre = { version = "*" } lettre = { version = "*" }
lettre_email = { version = "*" } lettre_email = { version = "*" }

View File

@ -1,16 +1,13 @@
use actix::{Handler, Message}; use actix::{Handler, Message};
use diesel::pg::Pg;
use diesel::prelude::*;
use serde::{Deserialize, Serialize};
use jirs_data::{Token, User}; use jirs_data::User;
use crate::{ use crate::{
db::{DbExecutor, DbPool, SyncQuery}, db::{tokens::FindAccessToken, DbExecutor, DbPool, DbPooledConn, SyncQuery},
db_pool,
errors::ServiceErrors, errors::ServiceErrors,
}; };
#[derive(Serialize, Deserialize, Debug)]
pub struct AuthorizeUser { pub struct AuthorizeUser {
pub access_token: uuid::Uuid, pub access_token: uuid::Uuid,
} }
@ -19,30 +16,26 @@ impl Message for AuthorizeUser {
type Result = Result<User, ServiceErrors>; type Result = Result<User, ServiceErrors>;
} }
impl AuthorizeUser {
pub fn execute(&self, conn: &DbPooledConn) -> Result<User, ServiceErrors> {
let token = FindAccessToken {
token: self.access_token,
}
.execute(conn)?;
crate::db::users::FindUser {
user_id: token.user_id,
}
.execute(conn)
}
}
impl Handler<AuthorizeUser> for DbExecutor { impl Handler<AuthorizeUser> for DbExecutor {
type Result = Result<User, ServiceErrors>; type Result = Result<User, ServiceErrors>;
fn handle(&mut self, msg: AuthorizeUser, _: &mut Self::Context) -> Self::Result { fn handle(&mut self, msg: AuthorizeUser, _: &mut Self::Context) -> Self::Result {
use crate::schema::tokens::dsl::{access_token, tokens}; let conn = db_pool!(self);
use crate::schema::users::dsl::{id, users}; msg.execute(conn)
let conn = &self
.pool
.get()
.map_err(|_| ServiceErrors::DatabaseConnectionLost)?;
let token = tokens
.filter(access_token.eq(msg.access_token))
.first::<Token>(conn)
.map_err(|_e| {
ServiceErrors::RecordNotFound(format!("token for {}", msg.access_token))
})?;
let user_query = users.filter(id.eq(token.user_id));
debug!("{}", diesel::debug_query::<Pg, _>(&user_query));
user_query
.first::<User>(conn)
.map_err(|_e| ServiceErrors::RecordNotFound(format!("user {}", token.user_id)))
} }
} }
@ -50,21 +43,10 @@ impl SyncQuery for AuthorizeUser {
type Result = std::result::Result<User, crate::errors::ServiceErrors>; type Result = std::result::Result<User, crate::errors::ServiceErrors>;
fn handle(&self, pool: &DbPool) -> Self::Result { fn handle(&self, pool: &DbPool) -> Self::Result {
use crate::schema::tokens::dsl::{access_token, tokens}; let conn = pool.get().map_err(|e| {
use crate::schema::users::dsl::{id, users}; error!("{:?}", e);
crate::errors::ServiceErrors::DatabaseConnectionLost
let conn = pool })?;
.get() self.execute(&conn)
.map_err(|_| crate::errors::ServiceErrors::DatabaseConnectionLost)?;
let token = tokens
.filter(access_token.eq(self.access_token))
.first::<Token>(&conn)
.map_err(|_| crate::errors::ServiceErrors::Unauthorized)?;
let user_query = users.filter(id.eq(token.user_id));
debug!("{}", diesel::debug_query::<Pg, _>(&user_query));
user_query
.first::<User>(&conn)
.map_err(|_| crate::errors::ServiceErrors::Unauthorized)
} }
} }

View File

@ -1,17 +1,32 @@
use actix::{Handler, Message}; use actix::{Handler, Message};
use diesel::pg::Pg;
use diesel::prelude::*; use diesel::prelude::*;
use serde::{Deserialize, Serialize};
use jirs_data::Comment; use jirs_data::{msg::WsError, Comment};
use crate::{db::DbExecutor, errors::ServiceErrors}; use crate::{
db::{DbExecutor, DbPooledConn},
db_pool,
errors::ServiceErrors,
q,
};
#[derive(Serialize, Deserialize)]
pub struct LoadIssueComments { pub struct LoadIssueComments {
pub issue_id: i32, pub issue_id: i32,
} }
impl LoadIssueComments {
pub fn execute(self, conn: &DbPooledConn) -> Result<Vec<Comment>, ServiceErrors> {
use crate::schema::comments::dsl::*;
q!(comments.distinct_on(id).filter(issue_id.eq(self.issue_id)))
.load(conn)
.map_err(|e| {
error!("{:?}", e);
ServiceErrors::Error(WsError::FailedToLoadComments)
})
}
}
impl Message for LoadIssueComments { impl Message for LoadIssueComments {
type Result = Result<Vec<Comment>, ServiceErrors>; type Result = Result<Vec<Comment>, ServiceErrors>;
} }
@ -20,28 +35,33 @@ impl Handler<LoadIssueComments> for DbExecutor {
type Result = Result<Vec<Comment>, ServiceErrors>; type Result = Result<Vec<Comment>, ServiceErrors>;
fn handle(&mut self, msg: LoadIssueComments, _ctx: &mut Self::Context) -> Self::Result { fn handle(&mut self, msg: LoadIssueComments, _ctx: &mut Self::Context) -> Self::Result {
use crate::schema::comments::dsl::*; let conn = db_pool!(self);
msg.execute(conn)
let conn = &self
.pool
.get()
.map_err(|_| ServiceErrors::DatabaseConnectionLost)?;
let comments_query = comments.distinct_on(id).filter(issue_id.eq(msg.issue_id));
debug!("{}", diesel::debug_query::<Pg, _>(&comments_query));
comments_query
.load(conn)
.map_err(|_| ServiceErrors::RecordNotFound("issue comments".to_string()))
} }
} }
#[derive(Serialize, Deserialize)]
pub struct CreateComment { pub struct CreateComment {
pub user_id: i32, pub user_id: i32,
pub issue_id: i32, pub issue_id: i32,
pub body: String, pub body: String,
} }
impl CreateComment {
pub fn execute(self, conn: &DbPooledConn) -> Result<Comment, ServiceErrors> {
use crate::schema::comments::dsl::*;
q!(diesel::insert_into(comments).values((
body.eq(self.body),
user_id.eq(self.user_id),
issue_id.eq(self.issue_id),
)))
.get_result::<Comment>(conn)
.map_err(|e| {
error!("{:?}", e);
ServiceErrors::Error(WsError::InvalidComment)
})
}
}
impl Message for CreateComment { impl Message for CreateComment {
type Result = Result<Comment, ServiceErrors>; type Result = Result<Comment, ServiceErrors>;
} }
@ -50,35 +70,35 @@ impl Handler<CreateComment> for DbExecutor {
type Result = Result<Comment, ServiceErrors>; type Result = Result<Comment, ServiceErrors>;
fn handle(&mut self, msg: CreateComment, _ctx: &mut Self::Context) -> Self::Result { fn handle(&mut self, msg: CreateComment, _ctx: &mut Self::Context) -> Self::Result {
use crate::models::CommentForm; let conn = db_pool!(self);
use crate::schema::comments::dsl::*; msg.execute(conn)
let form = CommentForm {
body: msg.body,
user_id: msg.user_id,
issue_id: msg.issue_id,
};
let conn = &self
.pool
.get()
.map_err(|_| ServiceErrors::DatabaseConnectionLost)?;
let comment_query = diesel::insert_into(comments).values(form);
debug!("{}", diesel::debug_query::<Pg, _>(&comment_query));
comment_query
.get_result::<Comment>(conn)
.map_err(|_| ServiceErrors::RecordNotFound("issue comments".to_string()))
} }
} }
#[derive(Serialize, Deserialize)]
pub struct UpdateComment { pub struct UpdateComment {
pub comment_id: i32, pub comment_id: i32,
pub user_id: i32, pub user_id: i32,
pub body: String, pub body: String,
} }
impl UpdateComment {
pub fn execute(self, conn: &DbPooledConn) -> Result<Comment, ServiceErrors> {
use crate::schema::comments::dsl::*;
q!(diesel::update(
comments
.filter(user_id.eq(self.user_id))
.find(self.comment_id),
)
.set(body.eq(self.body)))
.get_result::<Comment>(conn)
.map_err(|e| {
error!("{:?}", e);
ServiceErrors::Error(WsError::FailedToUpdateComment)
})
}
}
impl Message for UpdateComment { impl Message for UpdateComment {
type Result = Result<Comment, ServiceErrors>; type Result = Result<Comment, ServiceErrors>;
} }
@ -87,32 +107,32 @@ impl Handler<UpdateComment> for DbExecutor {
type Result = Result<Comment, ServiceErrors>; type Result = Result<Comment, ServiceErrors>;
fn handle(&mut self, msg: UpdateComment, _ctx: &mut Self::Context) -> Self::Result { fn handle(&mut self, msg: UpdateComment, _ctx: &mut Self::Context) -> Self::Result {
use crate::schema::comments::dsl::*; let conn = db_pool!(self);
msg.execute(conn)
let conn = &self
.pool
.get()
.map_err(|_| ServiceErrors::DatabaseConnectionLost)?;
let query = diesel::update(
comments
.filter(user_id.eq(msg.user_id))
.find(msg.comment_id),
)
.set(body.eq(msg.body));
info!("{}", diesel::debug_query::<Pg, _>(&query));
let row: Comment = query
.get_result::<Comment>(conn)
.map_err(|_| ServiceErrors::RecordNotFound("issue comments".to_string()))?;
Ok(row)
} }
} }
#[derive(Serialize, Deserialize)]
pub struct DeleteComment { pub struct DeleteComment {
pub comment_id: i32, pub comment_id: i32,
pub user_id: i32, pub user_id: i32,
} }
impl DeleteComment {
pub fn execute(self, conn: &DbPooledConn) -> Result<usize, ServiceErrors> {
use crate::schema::comments::dsl::*;
q!(diesel::delete(
comments
.filter(user_id.eq(self.user_id))
.find(self.comment_id),
))
.execute(conn)
.map_err(|e| {
error!("{:?}", e);
ServiceErrors::Error(WsError::UnableToDeleteComment)
})
}
}
impl Message for DeleteComment { impl Message for DeleteComment {
type Result = Result<(), ServiceErrors>; type Result = Result<(), ServiceErrors>;
} }
@ -121,22 +141,8 @@ impl Handler<DeleteComment> for DbExecutor {
type Result = Result<(), ServiceErrors>; type Result = Result<(), ServiceErrors>;
fn handle(&mut self, msg: DeleteComment, _ctx: &mut Self::Context) -> Self::Result { fn handle(&mut self, msg: DeleteComment, _ctx: &mut Self::Context) -> Self::Result {
use crate::schema::comments::dsl::*; let conn = db_pool!(self);
msg.execute(conn)?;
let conn = &self
.pool
.get()
.map_err(|_| ServiceErrors::DatabaseConnectionLost)?;
let comment_query = diesel::delete(
comments
.filter(user_id.eq(msg.user_id))
.find(msg.comment_id),
);
debug!("{}", diesel::debug_query::<Pg, _>(&comment_query));
comment_query
.execute(conn)
.map_err(|_| ServiceErrors::RecordNotFound("issue comments".to_string()))?;
Ok(()) Ok(())
} }
} }

View File

@ -1,13 +1,10 @@
use actix::{Handler, Message}; use actix::{Handler, Message};
use diesel::pg::Pg;
use diesel::prelude::*; use diesel::prelude::*;
use serde::{Deserialize, Serialize};
use jirs_data::Epic; use jirs_data::{msg::WsError, Epic};
use crate::{db::DbExecutor, errors::ServiceErrors}; use crate::{db::DbExecutor, db_pool, errors::ServiceErrors, q};
#[derive(Serialize, Deserialize)]
pub struct LoadEpics { pub struct LoadEpics {
pub project_id: i32, pub project_id: i32,
} }
@ -22,20 +19,17 @@ impl Handler<LoadEpics> for DbExecutor {
fn handle(&mut self, msg: LoadEpics, _ctx: &mut Self::Context) -> Self::Result { fn handle(&mut self, msg: LoadEpics, _ctx: &mut Self::Context) -> Self::Result {
use crate::schema::epics::dsl::*; use crate::schema::epics::dsl::*;
let conn = &self let conn = db_pool!(self);
.pool
.get()
.map_err(|_| ServiceErrors::DatabaseConnectionLost)?;
let epics_query = epics.distinct_on(id).filter(project_id.eq(msg.project_id)); q!(epics.distinct_on(id).filter(project_id.eq(msg.project_id)))
debug!("{}", diesel::debug_query::<Pg, _>(&epics_query));
epics_query
.load(conn) .load(conn)
.map_err(|_| ServiceErrors::RecordNotFound("epics".to_string())) .map_err(|e| {
error!("{:?}", e);
ServiceErrors::Error(WsError::FailedToLoadEpics)
})
} }
} }
#[derive(Serialize, Deserialize)]
pub struct CreateEpic { pub struct CreateEpic {
pub user_id: i32, pub user_id: i32,
pub project_id: i32, pub project_id: i32,
@ -52,24 +46,21 @@ impl Handler<CreateEpic> for DbExecutor {
fn handle(&mut self, msg: CreateEpic, _ctx: &mut Self::Context) -> Self::Result { fn handle(&mut self, msg: CreateEpic, _ctx: &mut Self::Context) -> Self::Result {
use crate::schema::epics::dsl::*; use crate::schema::epics::dsl::*;
let conn = &self let conn = db_pool!(self);
.pool
.get()
.map_err(|_| ServiceErrors::DatabaseConnectionLost)?;
let epic_query = diesel::insert_into(epics).values(( q!(diesel::insert_into(epics).values((
name.eq(msg.name.as_str()), name.eq(msg.name.as_str()),
user_id.eq(msg.user_id), user_id.eq(msg.user_id),
project_id.eq(msg.project_id), project_id.eq(msg.project_id),
)); )))
debug!("{}", diesel::debug_query::<Pg, _>(&epic_query)); .get_result::<Epic>(conn)
epic_query .map_err(|e| {
.get_result::<Epic>(conn) error!("{:?}", e);
.map_err(|_| ServiceErrors::RecordNotFound("epics".to_string())) ServiceErrors::Error(WsError::InvalidEpic)
})
} }
} }
#[derive(Serialize, Deserialize)]
pub struct UpdateEpic { pub struct UpdateEpic {
pub epic_id: i32, pub epic_id: i32,
pub project_id: i32, pub project_id: i32,
@ -86,25 +77,22 @@ impl Handler<UpdateEpic> for DbExecutor {
fn handle(&mut self, msg: UpdateEpic, _ctx: &mut Self::Context) -> Self::Result { fn handle(&mut self, msg: UpdateEpic, _ctx: &mut Self::Context) -> Self::Result {
use crate::schema::epics::dsl::*; use crate::schema::epics::dsl::*;
let conn = &self let conn = db_pool!(self);
.pool
.get() q!(diesel::update(
.map_err(|_| ServiceErrors::DatabaseConnectionLost)?;
let query = diesel::update(
epics epics
.filter(project_id.eq(msg.project_id)) .filter(project_id.eq(msg.project_id))
.find(msg.epic_id), .find(msg.epic_id),
) )
.set(name.eq(msg.name)); .set(name.eq(msg.name)))
info!("{}", diesel::debug_query::<Pg, _>(&query)); .get_result::<Epic>(conn)
let row: Epic = query .map_err(|e| {
.get_result::<Epic>(conn) error!("{:?}", e);
.map_err(|_| ServiceErrors::RecordNotFound("epics".to_string()))?; ServiceErrors::Error(WsError::FailedToUpdateEpic)
Ok(row) })
} }
} }
#[derive(Serialize, Deserialize)]
pub struct DeleteEpic { pub struct DeleteEpic {
pub epic_id: i32, pub epic_id: i32,
pub user_id: i32, pub user_id: i32,
@ -120,16 +108,17 @@ impl Handler<DeleteEpic> for DbExecutor {
fn handle(&mut self, msg: DeleteEpic, _ctx: &mut Self::Context) -> Self::Result { fn handle(&mut self, msg: DeleteEpic, _ctx: &mut Self::Context) -> Self::Result {
use crate::schema::epics::dsl::*; use crate::schema::epics::dsl::*;
let conn = &self let conn = db_pool!(self);
.pool
.get() q!(diesel::delete(
.map_err(|_| ServiceErrors::DatabaseConnectionLost)?; epics.filter(user_id.eq(msg.user_id)).find(msg.epic_id)
))
.execute(conn)
.map_err(|e| {
error!("{:?}", e);
ServiceErrors::Error(WsError::UnableToDeleteEpic)
})?;
let comment_query = diesel::delete(epics.filter(user_id.eq(msg.user_id)).find(msg.epic_id));
debug!("{}", diesel::debug_query::<Pg, _>(&comment_query));
comment_query
.execute(conn)
.map_err(|_| ServiceErrors::RecordNotFound("epics".to_string()))?;
Ok(()) Ok(())
} }
} }

View File

@ -1,22 +1,49 @@
use actix::{Handler, Message}; use actix::{Handler, Message};
use diesel::connection::TransactionManager;
use diesel::pg::Pg;
use diesel::prelude::*; use diesel::prelude::*;
use jirs_data::{ use jirs_data::{
EmailString, Invitation, InvitationId, InvitationState, InvitationToken, ProjectId, Token, msg::WsError, EmailString, Invitation, InvitationId, InvitationState, InvitationToken,
User, UserId, UserRole, UsernameString, ProjectId, Token, User, UserId, UserRole, UsernameString,
}; };
use crate::db::DbPooledConn;
use crate::{ use crate::{
db::{ db::{
tokens::CreateBindToken, tokens::CreateBindToken,
users::{LookupUser, Register}, users::{LookupUser, Register},
DbExecutor, DbExecutor,
}, },
db_pool,
errors::ServiceErrors, errors::ServiceErrors,
q,
}; };
pub struct FindByBindToken {
pub token: InvitationToken,
}
impl FindByBindToken {
pub fn execute(self, conn: &DbPooledConn) -> Result<Invitation, ServiceErrors> {
use crate::schema::invitations::dsl::*;
q!(invitations.filter(bind_token.eq(self.token)))
.first(conn)
.map_err(|e| ServiceErrors::DatabaseQueryFailed(format!("{}", e)))
}
}
impl Message for FindByBindToken {
type Result = Result<Invitation, ServiceErrors>;
}
impl Handler<FindByBindToken> for DbExecutor {
type Result = Result<Invitation, ServiceErrors>;
fn handle(&mut self, msg: FindByBindToken, _ctx: &mut Self::Context) -> Self::Result {
let conn = db_pool!(self);
msg.execute(conn)
}
}
pub struct ListInvitation { pub struct ListInvitation {
pub user_id: UserId, pub user_id: UserId,
} }
@ -31,20 +58,18 @@ impl Handler<ListInvitation> for DbExecutor {
fn handle(&mut self, msg: ListInvitation, _ctx: &mut Self::Context) -> Self::Result { fn handle(&mut self, msg: ListInvitation, _ctx: &mut Self::Context) -> Self::Result {
use crate::schema::invitations::dsl::*; use crate::schema::invitations::dsl::*;
let conn = &self let conn = db_pool!(self);
.pool
.get()
.map_err(|_| ServiceErrors::DatabaseConnectionLost)?;
let query = invitations q!(invitations
.filter(invited_by_id.eq(msg.user_id)) .filter(invited_by_id.eq(msg.user_id))
.filter(state.ne(InvitationState::Accepted)) .filter(state.ne(InvitationState::Accepted))
.order_by(state.asc()) .order_by(state.asc())
.then_order_by(updated_at.desc()); .then_order_by(updated_at.desc()))
debug!("{}", diesel::debug_query::<Pg, _>(&query).to_string()); .load(conn)
query .map_err(|e| {
.load(conn) error!("{:?}", e);
.map_err(|_| ServiceErrors::DatabaseConnectionLost) ServiceErrors::Error(WsError::FailedToLoadInvitations)
})
} }
} }
@ -56,6 +81,25 @@ pub struct CreateInvitation {
pub role: UserRole, pub role: UserRole,
} }
impl CreateInvitation {
pub fn execute(self, conn: &DbPooledConn) -> Result<Invitation, ServiceErrors> {
use crate::schema::invitations::dsl::*;
q!(diesel::insert_into(invitations).values((
name.eq(self.name),
email.eq(self.email),
state.eq(InvitationState::Sent),
project_id.eq(self.project_id),
invited_by_id.eq(self.user_id),
role.eq(self.role),
)))
.get_result(conn)
.map_err(|e| {
error!("{:?}", e);
ServiceErrors::Error(WsError::InvalidInvitation)
})
}
}
impl Message for CreateInvitation { impl Message for CreateInvitation {
type Result = Result<Invitation, ServiceErrors>; type Result = Result<Invitation, ServiceErrors>;
} }
@ -64,25 +108,8 @@ impl Handler<CreateInvitation> for DbExecutor {
type Result = Result<Invitation, ServiceErrors>; type Result = Result<Invitation, ServiceErrors>;
fn handle(&mut self, msg: CreateInvitation, _ctx: &mut Self::Context) -> Self::Result { fn handle(&mut self, msg: CreateInvitation, _ctx: &mut Self::Context) -> Self::Result {
use crate::schema::invitations::dsl::*; let conn = db_pool!(self);
msg.execute(conn)
let conn = &self
.pool
.get()
.map_err(|e| ServiceErrors::DatabaseQueryFailed(format!("{}", e)))?;
let query = diesel::insert_into(invitations).values((
name.eq(msg.name),
email.eq(msg.email),
state.eq(InvitationState::Sent),
project_id.eq(msg.project_id),
invited_by_id.eq(msg.user_id),
role.eq(msg.role),
));
debug!("{}", diesel::debug_query::<Pg, _>(&query).to_string());
query
.get_result(conn)
.map_err(|e| ServiceErrors::DatabaseQueryFailed(format!("{}", e)))
} }
} }
@ -90,6 +117,18 @@ pub struct DeleteInvitation {
pub id: InvitationId, pub id: InvitationId,
} }
impl DeleteInvitation {
pub fn execute(self, conn: &DbPooledConn) -> Result<usize, ServiceErrors> {
use crate::schema::invitations::dsl::*;
q!(diesel::delete(invitations).filter(id.eq(self.id)))
.execute(conn)
.map_err(|e| {
error!("{:?}", e);
ServiceErrors::Error(WsError::UnableToDeleteInvitation)
})
}
}
impl Message for DeleteInvitation { impl Message for DeleteInvitation {
type Result = Result<(), ServiceErrors>; type Result = Result<(), ServiceErrors>;
} }
@ -98,17 +137,8 @@ impl Handler<DeleteInvitation> for DbExecutor {
type Result = Result<(), ServiceErrors>; type Result = Result<(), ServiceErrors>;
fn handle(&mut self, msg: DeleteInvitation, _ctx: &mut Self::Context) -> Self::Result { fn handle(&mut self, msg: DeleteInvitation, _ctx: &mut Self::Context) -> Self::Result {
use crate::schema::invitations::dsl::*; let conn = db_pool!(self);
msg.execute(conn)?;
let conn = &self
.pool
.get()
.map_err(|_| ServiceErrors::DatabaseConnectionLost)?;
let query = diesel::delete(invitations).filter(id.eq(msg.id));
debug!("{}", diesel::debug_query::<Pg, _>(&query).to_string());
query
.execute(conn)
.map_err(|e| ServiceErrors::DatabaseQueryFailed(format!("{}", e)))?;
Ok(()) Ok(())
} }
} }
@ -118,6 +148,24 @@ struct UpdateInvitationState {
pub state: InvitationState, pub state: InvitationState,
} }
impl UpdateInvitationState {
pub fn execute(self, conn: &DbPooledConn) -> Result<usize, ServiceErrors> {
use crate::schema::invitations::dsl::*;
q!(diesel::update(invitations)
.set((
state.eq(self.state),
updated_at.eq(chrono::Utc::now().naive_utc()),
))
.filter(id.eq(self.id)))
.execute(conn)
.map_err(|e| {
error!("{:?}", e);
ServiceErrors::Error(WsError::FailedToUpdateInvitation)
})
}
}
impl Message for UpdateInvitationState { impl Message for UpdateInvitationState {
type Result = Result<(), ServiceErrors>; type Result = Result<(), ServiceErrors>;
} }
@ -126,23 +174,8 @@ impl Handler<UpdateInvitationState> for DbExecutor {
type Result = Result<(), ServiceErrors>; type Result = Result<(), ServiceErrors>;
fn handle(&mut self, msg: UpdateInvitationState, _ctx: &mut Self::Context) -> Self::Result { fn handle(&mut self, msg: UpdateInvitationState, _ctx: &mut Self::Context) -> Self::Result {
use crate::schema::invitations::dsl::*; let conn = db_pool!(self);
msg.execute(conn)?;
let conn = &self
.pool
.get()
.map_err(|_| ServiceErrors::DatabaseConnectionLost)?;
let query = diesel::update(invitations)
.set((
state.eq(msg.state),
updated_at.eq(chrono::Utc::now().naive_utc()),
))
.filter(id.eq(msg.id));
debug!("{}", diesel::debug_query::<Pg, _>(&query).to_string());
query
.execute(conn)
.map_err(|e| ServiceErrors::DatabaseQueryFailed(format!("{}", e)))?;
Ok(()) Ok(())
} }
} }
@ -158,14 +191,14 @@ impl Message for RevokeInvitation {
impl Handler<RevokeInvitation> for DbExecutor { impl Handler<RevokeInvitation> for DbExecutor {
type Result = Result<(), ServiceErrors>; type Result = Result<(), ServiceErrors>;
fn handle(&mut self, msg: RevokeInvitation, ctx: &mut Self::Context) -> Self::Result { fn handle(&mut self, msg: RevokeInvitation, _ctx: &mut Self::Context) -> Self::Result {
self.handle( let conn = db_pool!(self);
UpdateInvitationState { UpdateInvitationState {
id: msg.id, id: msg.id,
state: InvitationState::Revoked, state: InvitationState::Revoked,
}, }
ctx, .execute(conn)?;
) Ok(())
} }
} }
@ -173,6 +206,85 @@ pub struct AcceptInvitation {
pub invitation_token: InvitationToken, pub invitation_token: InvitationToken,
} }
impl AcceptInvitation {
pub fn execute(self, conn: &DbPooledConn) -> Result<Token, ServiceErrors> {
use crate::schema::invitations::dsl::*;
crate::db::Guard::new(conn)?.run::<Token, _>(|_guard| {
let invitation = crate::db::invitations::FindByBindToken {
token: self.invitation_token,
}
.execute(conn)?;
if invitation.state == InvitationState::Revoked {
return Err(ServiceErrors::Error(WsError::InvitationRevoked));
}
crate::db::invitations::UpdateInvitationState {
id: invitation.id,
state: InvitationState::Accepted,
}
.execute(conn)?;
q!(diesel::update(invitations)
.set((
state.eq(InvitationState::Accepted),
updated_at.eq(chrono::Utc::now().naive_utc()),
))
.filter(id.eq(invitation.id))
.filter(state.eq(InvitationState::Sent)))
.execute(conn)
.map_err(|e| {
ServiceErrors::DatabaseQueryFailed(format!(
"update invitation {} {}",
invitation.id, e
))
})?;
match {
Register {
name: invitation.name.clone(),
email: invitation.email.clone(),
project_id: Some(invitation.project_id),
role: UserRole::User,
}
.execute(conn)
} {
Ok(_) => (),
Err(ServiceErrors::Error(WsError::InvalidPair(..))) => (),
Err(e) => return Err(e),
};
let user: User = LookupUser {
name: invitation.name.clone(),
email: invitation.email.clone(),
}
.execute(conn)?;
CreateBindToken { user_id: user.id }.execute(conn)?;
self.bind_to_default_project(conn, &invitation, &user)?;
crate::db::tokens::FindUserId { user_id: user.id }.execute(conn)
})
}
fn bind_to_default_project(
&self,
conn: &DbPooledConn,
invitation: &Invitation,
user: &User,
) -> Result<usize, ServiceErrors> {
crate::db::user_projects::CreateUserProject {
user_id: user.id,
project_id: invitation.project_id,
is_current: false,
is_default: false,
role: invitation.role,
}
.execute(conn)
}
}
impl Message for AcceptInvitation { impl Message for AcceptInvitation {
type Result = Result<Token, ServiceErrors>; type Result = Result<Token, ServiceErrors>;
} }
@ -180,107 +292,9 @@ impl Message for AcceptInvitation {
impl Handler<AcceptInvitation> for DbExecutor { impl Handler<AcceptInvitation> for DbExecutor {
type Result = Result<Token, ServiceErrors>; type Result = Result<Token, ServiceErrors>;
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::*; let conn = db_pool!(self);
let conn = &self msg.execute(conn)
.pool
.get()
.map_err(|_| ServiceErrors::DatabaseConnectionLost)?;
let tm = conn.transaction_manager();
tm.begin_transaction(conn)
.map_err(|_| ServiceErrors::DatabaseConnectionLost)?;
let query = invitations.filter(bind_token.eq(msg.invitation_token));
debug!("{}", diesel::debug_query::<Pg, _>(&query).to_string());
let invitation: Invitation = query.first(conn).map_err(|e| {
if tm.rollback_transaction(conn).is_err() {
return ServiceErrors::DatabaseConnectionLost;
}
ServiceErrors::DatabaseQueryFailed(format!("{}", e))
})?;
if invitation.state == InvitationState::Revoked {
if tm.rollback_transaction(conn).is_err() {
return Err(ServiceErrors::DatabaseConnectionLost);
}
return Err(ServiceErrors::DatabaseQueryFailed(
"This invitation is no longer valid".to_string(),
));
}
let query = diesel::update(invitations)
.set((
state.eq(InvitationState::Accepted),
updated_at.eq(chrono::Utc::now().naive_utc()),
))
.filter(id.eq(invitation.id))
.filter(state.eq(InvitationState::Sent));
debug!("{}", diesel::debug_query::<Pg, _>(&query).to_string());
query.execute(conn).map_err(|e| {
if tm.rollback_transaction(conn).is_err() {
return ServiceErrors::DatabaseConnectionLost;
}
ServiceErrors::DatabaseQueryFailed(format!("update invitation {} {}", invitation.id, e))
})?;
match self.handle(
Register {
name: invitation.name.clone(),
email: invitation.email.clone(),
project_id: Some(invitation.project_id),
},
ctx,
) {
Ok(_) => (),
Err(ServiceErrors::RegisterCollision) => (),
Err(e) => return Err(e),
};
let user: User = self.handle(
LookupUser {
name: invitation.name.clone(),
email: invitation.email.clone(),
},
ctx,
)?;
self.handle(CreateBindToken { user_id: user.id }, ctx)?;
{
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),
role.eq(invitation.role),
));
debug!("{}", diesel::debug_query::<Pg, _>(&query));
query.execute(conn).map_err(|e| {
if tm.rollback_transaction(conn).is_err() {
return ServiceErrors::DatabaseConnectionLost;
}
ServiceErrors::DatabaseQueryFailed(format!("{}", e))
})?;
};
let token = {
use crate::schema::tokens::dsl::*;
let query = tokens.filter(user_id.eq(user.id)).order_by(id.desc());
debug!("{}", diesel::debug_query::<Pg, _>(&query));
query.first(conn).map_err(|e| {
if tm.rollback_transaction(conn).is_err() {
return ServiceErrors::DatabaseConnectionLost;
}
ServiceErrors::DatabaseQueryFailed(format!("token for user {} {}", user.id, e))
})?
};
tm.commit_transaction(conn)
.map_err(|_| ServiceErrors::DatabaseConnectionLost)?;
Ok(token)
} }
} }

View File

@ -1,17 +1,34 @@
use actix::{Handler, Message}; use actix::{Handler, Message};
use diesel::pg::Pg;
use diesel::prelude::*; use diesel::prelude::*;
use serde::{Deserialize, Serialize};
use jirs_data::IssueAssignee; use jirs_data::IssueAssignee;
use crate::{db::DbExecutor, errors::ServiceErrors}; use crate::{
db::{DbExecutor, DbPooledConn},
db_pool,
errors::ServiceErrors,
q,
};
#[derive(Serialize, Deserialize)]
pub struct LoadAssignees { pub struct LoadAssignees {
pub issue_id: i32, pub issue_id: i32,
} }
impl LoadAssignees {
pub fn execute(self, conn: &DbPooledConn) -> Result<Vec<IssueAssignee>, ServiceErrors> {
use crate::schema::issue_assignees::dsl::*;
q!(issue_assignees
.distinct_on(id)
.filter(issue_id.eq(self.issue_id)))
.load::<IssueAssignee>(conn)
.map_err(|e| {
error!("{:?}", e);
ServiceErrors::RecordNotFound("issue users".to_string())
})
}
}
impl Message for LoadAssignees { impl Message for LoadAssignees {
type Result = Result<Vec<IssueAssignee>, ServiceErrors>; type Result = Result<Vec<IssueAssignee>, ServiceErrors>;
} }
@ -20,18 +37,8 @@ impl Handler<LoadAssignees> for DbExecutor {
type Result = Result<Vec<IssueAssignee>, ServiceErrors>; type Result = Result<Vec<IssueAssignee>, ServiceErrors>;
fn handle(&mut self, msg: LoadAssignees, _ctx: &mut Self::Context) -> Self::Result { fn handle(&mut self, msg: LoadAssignees, _ctx: &mut Self::Context) -> Self::Result {
use crate::schema::issue_assignees::dsl::*; let conn = db_pool!(self);
let conn = &self msg.execute(conn)
.pool
.get()
.map_err(|_| ServiceErrors::DatabaseConnectionLost)?;
let issue_assignees_query = issue_assignees
.distinct_on(id)
.filter(issue_id.eq(msg.issue_id));
debug!("{}", diesel::debug_query::<Pg, _>(&issue_assignees_query));
issue_assignees_query
.load::<IssueAssignee>(conn)
.map_err(|_| ServiceErrors::RecordNotFound("issue users".to_string()))
} }
} }

View File

@ -1,15 +1,30 @@
use actix::{Handler, Message}; use actix::{Handler, Message};
use diesel::pg::Pg;
use diesel::prelude::*; use diesel::prelude::*;
use jirs_data::{IssueStatus, IssueStatusId, Position, ProjectId, TitleString}; use jirs_data::{IssueStatus, IssueStatusId, Position, ProjectId, TitleString};
use crate::{db::DbExecutor, errors::ServiceErrors}; use crate::db::DbPooledConn;
use crate::{db::DbExecutor, db_pool, errors::ServiceErrors, q};
pub struct LoadIssueStatuses { pub struct LoadIssueStatuses {
pub project_id: ProjectId, pub project_id: ProjectId,
} }
impl LoadIssueStatuses {
pub fn execute(self, conn: &DbPooledConn) -> Result<Vec<IssueStatus>, ServiceErrors> {
use crate::schema::issue_statuses::dsl::{id, issue_statuses, project_id};
q!(issue_statuses
.distinct_on(id)
.filter(project_id.eq(self.project_id)))
.load::<IssueStatus>(conn)
.map_err(|e| {
error!("{:?}", e);
ServiceErrors::RecordNotFound("issue users".to_string())
})
}
}
impl Message for LoadIssueStatuses { impl Message for LoadIssueStatuses {
type Result = Result<Vec<IssueStatus>, ServiceErrors>; type Result = Result<Vec<IssueStatus>, ServiceErrors>;
} }
@ -18,20 +33,9 @@ impl Handler<LoadIssueStatuses> for DbExecutor {
type Result = Result<Vec<IssueStatus>, ServiceErrors>; type Result = Result<Vec<IssueStatus>, ServiceErrors>;
fn handle(&mut self, msg: LoadIssueStatuses, _ctx: &mut Self::Context) -> Self::Result { fn handle(&mut self, msg: LoadIssueStatuses, _ctx: &mut Self::Context) -> Self::Result {
use crate::schema::issue_statuses::dsl::{id, issue_statuses, project_id}; let conn = db_pool!(self);
let conn = &self msg.execute(conn)
.pool
.get()
.map_err(|_| ServiceErrors::DatabaseConnectionLost)?;
let issue_assignees_query = issue_statuses
.distinct_on(id)
.filter(project_id.eq(msg.project_id));
debug!("{}", diesel::debug_query::<Pg, _>(&issue_assignees_query));
issue_assignees_query
.load::<IssueStatus>(conn)
.map_err(|_| ServiceErrors::RecordNotFound("issue users".to_string()))
} }
} }
@ -41,6 +45,22 @@ pub struct CreateIssueStatus {
pub name: TitleString, pub name: TitleString,
} }
impl CreateIssueStatus {
pub fn execute(self, conn: &DbPooledConn) -> Result<IssueStatus, ServiceErrors> {
use crate::schema::issue_statuses::dsl::{issue_statuses, name, position, project_id};
q!(diesel::insert_into(issue_statuses).values((
project_id.eq(self.project_id),
name.eq(self.name),
position.eq(self.position),
)))
.get_result::<IssueStatus>(conn)
.map_err(|e| {
error!("{:?}", e);
ServiceErrors::RecordNotFound("issue users".to_string())
})
}
}
impl Message for CreateIssueStatus { impl Message for CreateIssueStatus {
type Result = Result<IssueStatus, ServiceErrors>; type Result = Result<IssueStatus, ServiceErrors>;
} }
@ -49,22 +69,9 @@ impl Handler<CreateIssueStatus> for DbExecutor {
type Result = Result<IssueStatus, ServiceErrors>; type Result = Result<IssueStatus, ServiceErrors>;
fn handle(&mut self, msg: CreateIssueStatus, _ctx: &mut Self::Context) -> Self::Result { fn handle(&mut self, msg: CreateIssueStatus, _ctx: &mut Self::Context) -> Self::Result {
use crate::schema::issue_statuses::dsl::{issue_statuses, name, position, project_id}; let conn = db_pool!(self);
let conn = &self msg.execute(conn)
.pool
.get()
.map_err(|_| ServiceErrors::DatabaseConnectionLost)?;
let issue_assignees_query = diesel::insert_into(issue_statuses).values((
project_id.eq(msg.project_id),
name.eq(msg.name),
position.eq(msg.position),
));
debug!("{}", diesel::debug_query::<Pg, _>(&issue_assignees_query));
issue_assignees_query
.get_result::<IssueStatus>(conn)
.map_err(|_| ServiceErrors::RecordNotFound("issue users".to_string()))
} }
} }
@ -83,18 +90,16 @@ impl Handler<DeleteIssueStatus> for DbExecutor {
fn handle(&mut self, msg: DeleteIssueStatus, _ctx: &mut Self::Context) -> Self::Result { fn handle(&mut self, msg: DeleteIssueStatus, _ctx: &mut Self::Context) -> Self::Result {
use crate::schema::issue_statuses::dsl::{id, issue_statuses, project_id}; use crate::schema::issue_statuses::dsl::{id, issue_statuses, project_id};
let conn = &self let conn = db_pool!(self);
.pool
.get()
.map_err(|_| ServiceErrors::DatabaseConnectionLost)?;
let issue_assignees_query = diesel::delete(issue_statuses) q!(diesel::delete(issue_statuses)
.filter(id.eq(msg.issue_status_id)) .filter(id.eq(msg.issue_status_id))
.filter(project_id.eq(msg.project_id)); .filter(project_id.eq(msg.project_id)))
debug!("{}", diesel::debug_query::<Pg, _>(&issue_assignees_query)); .execute(conn)
issue_assignees_query .map_err(|e| {
.execute(conn) error!("{:?}", e);
.map_err(|_| ServiceErrors::RecordNotFound("issue users".to_string()))?; ServiceErrors::RecordNotFound("issue users".to_string())
})?;
Ok(msg.issue_status_id) Ok(msg.issue_status_id)
} }
} }
@ -118,22 +123,20 @@ impl Handler<UpdateIssueStatus> for DbExecutor {
id, issue_statuses, name, position, project_id, updated_at, id, issue_statuses, name, position, project_id, updated_at,
}; };
let conn = &self let conn = db_pool!(self);
.pool
.get()
.map_err(|_| ServiceErrors::DatabaseConnectionLost)?;
let issue_assignees_query = diesel::update(issue_statuses) q!(diesel::update(issue_statuses)
.set(( .set((
name.eq(msg.name), name.eq(msg.name),
position.eq(msg.position), position.eq(msg.position),
updated_at.eq(chrono::Utc::now().naive_utc()), updated_at.eq(chrono::Utc::now().naive_utc()),
)) ))
.filter(id.eq(msg.issue_status_id)) .filter(id.eq(msg.issue_status_id))
.filter(project_id.eq(msg.project_id)); .filter(project_id.eq(msg.project_id)))
debug!("{}", diesel::debug_query::<Pg, _>(&issue_assignees_query)); .get_result::<IssueStatus>(conn)
issue_assignees_query .map_err(|e| {
.get_result::<IssueStatus>(conn) error!("{:?}", e);
.map_err(|_| ServiceErrors::RecordNotFound("issue users".to_string())) ServiceErrors::RecordNotFound("issue users".to_string())
})
} }
} }

View File

@ -4,9 +4,10 @@ use diesel::expression::sql_literal::sql;
use diesel::prelude::*; use diesel::prelude::*;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use jirs_data::msg::WsError;
use jirs_data::{IssuePriority, IssueStatusId, IssueType}; use jirs_data::{IssuePriority, IssueStatusId, IssueType};
use crate::{db::DbExecutor, errors::ServiceErrors, models::Issue}; use crate::{db::DbExecutor, db_pool, errors::ServiceErrors, models::Issue};
const FAILED_CONNECT_USER_AND_ISSUE: &str = "Failed to create connection between user and issue"; const FAILED_CONNECT_USER_AND_ISSUE: &str = "Failed to create connection between user and issue";
@ -24,19 +25,18 @@ impl Handler<LoadIssue> for DbExecutor {
fn handle(&mut self, msg: LoadIssue, _ctx: &mut Self::Context) -> Self::Result { fn handle(&mut self, msg: LoadIssue, _ctx: &mut Self::Context) -> Self::Result {
use crate::schema::issues::dsl::{id, issues}; use crate::schema::issues::dsl::{id, issues};
let conn = &self
.pool let conn = db_pool!(self);
.get()
.map_err(|_| ServiceErrors::DatabaseConnectionLost)?;
let query = issues.filter(id.eq(msg.issue_id)).distinct(); let query = issues.filter(id.eq(msg.issue_id)).distinct();
debug!( debug!(
"{}", "{}",
diesel::debug_query::<diesel::pg::Pg, _>(&query).to_string() diesel::debug_query::<diesel::pg::Pg, _>(&query).to_string()
); );
query query.first::<Issue>(conn).map_err(|e| {
.first::<Issue>(conn) error!("{:?}", e);
.map_err(|_| ServiceErrors::RecordNotFound("project issues".to_string())) ServiceErrors::RecordNotFound("project issues".to_string())
})
} }
} }
@ -54,18 +54,18 @@ impl Handler<LoadProjectIssues> for DbExecutor {
fn handle(&mut self, msg: LoadProjectIssues, _ctx: &mut Self::Context) -> Self::Result { fn handle(&mut self, msg: LoadProjectIssues, _ctx: &mut Self::Context) -> Self::Result {
use crate::schema::issues::dsl::{issues, project_id}; use crate::schema::issues::dsl::{issues, project_id};
let conn = &self
.pool let conn = db_pool!(self);
.get()
.map_err(|_| ServiceErrors::DatabaseConnectionLost)?;
let chain = issues.filter(project_id.eq(msg.project_id)).distinct(); let chain = issues.filter(project_id.eq(msg.project_id)).distinct();
debug!( debug!(
"{}", "{}",
diesel::debug_query::<diesel::pg::Pg, _>(&chain).to_string() diesel::debug_query::<diesel::pg::Pg, _>(&chain).to_string()
); );
let vec = chain let vec = chain.load::<Issue>(conn).map_err(|e| {
.load::<Issue>(conn) error!("{:?}", e);
.map_err(|_| ServiceErrors::RecordNotFound("project issues".to_string()))?; ServiceErrors::RecordNotFound("project issues".to_string())
})?;
Ok(vec) Ok(vec)
} }
} }
@ -98,10 +98,8 @@ impl Handler<UpdateIssue> for DbExecutor {
fn handle(&mut self, msg: UpdateIssue, _ctx: &mut Self::Context) -> Self::Result { fn handle(&mut self, msg: UpdateIssue, _ctx: &mut Self::Context) -> Self::Result {
use crate::schema::issues::dsl::{self, issues}; use crate::schema::issues::dsl::{self, issues};
let conn = &self
.pool let conn = db_pool!(self);
.get()
.map_err(|_| ServiceErrors::DatabaseConnectionLost)?;
let current_issue_id = msg.issue_id; let current_issue_id = msg.issue_id;
@ -133,7 +131,8 @@ impl Handler<UpdateIssue> for DbExecutor {
"{}", "{}",
diesel::debug_query::<diesel::pg::Pg, _>(&chain).to_string() diesel::debug_query::<diesel::pg::Pg, _>(&chain).to_string()
); );
chain.get_result::<Issue>(conn).map_err(|_| { chain.get_result::<Issue>(conn).map_err(|e| {
error!("{:?}", e);
ServiceErrors::DatabaseQueryFailed("Failed to update issue".to_string()) ServiceErrors::DatabaseQueryFailed("Failed to update issue".to_string())
})?; })?;
@ -142,12 +141,18 @@ impl Handler<UpdateIssue> for DbExecutor {
diesel::delete(dsl::issue_assignees) diesel::delete(dsl::issue_assignees)
.filter(not(dsl::user_id.eq_any(user_ids)).and(dsl::issue_id.eq(current_issue_id))) .filter(not(dsl::user_id.eq_any(user_ids)).and(dsl::issue_id.eq(current_issue_id)))
.execute(conn) .execute(conn)
.map_err(|_| ServiceErrors::DatabaseConnectionLost)?; .map_err(|e| {
error!("{:?}", e);
ServiceErrors::DatabaseConnectionLost
})?;
let existing: Vec<i32> = dsl::issue_assignees let existing: Vec<i32> = dsl::issue_assignees
.select(dsl::user_id) .select(dsl::user_id)
.filter(dsl::issue_id.eq(current_issue_id)) .filter(dsl::issue_id.eq(current_issue_id))
.get_results::<i32>(conn) .get_results::<i32>(conn)
.map_err(|_| ServiceErrors::DatabaseConnectionLost)?; .map_err(|e| {
error!("{:?}", e);
ServiceErrors::DatabaseConnectionLost
})?;
let mut values = vec![]; let mut values = vec![];
for user_id in user_ids.iter() { for user_id in user_ids.iter() {
if !existing.contains(user_id) { if !existing.contains(user_id) {
@ -160,15 +165,16 @@ impl Handler<UpdateIssue> for DbExecutor {
diesel::insert_into(dsl::issue_assignees) diesel::insert_into(dsl::issue_assignees)
.values(values) .values(values)
.execute(conn) .execute(conn)
.map_err(|_| { .map_err(|e| {
error!("{:?}", e);
ServiceErrors::DatabaseQueryFailed(FAILED_CONNECT_USER_AND_ISSUE.to_string()) ServiceErrors::DatabaseQueryFailed(FAILED_CONNECT_USER_AND_ISSUE.to_string())
})?; })?;
} }
issues issues.find(msg.issue_id).first::<Issue>(conn).map_err(|e| {
.find(msg.issue_id) error!("{:?}", e);
.first::<Issue>(conn) ServiceErrors::DatabaseConnectionLost
.map_err(|_| ServiceErrors::DatabaseConnectionLost) })
} }
} }
@ -188,10 +194,7 @@ impl Handler<DeleteIssue> for DbExecutor {
use crate::schema::issue_assignees::dsl::{issue_assignees, issue_id}; use crate::schema::issue_assignees::dsl::{issue_assignees, issue_id};
use crate::schema::issues::dsl::issues; use crate::schema::issues::dsl::issues;
let conn = &self let conn = db_pool!(self);
.pool
.get()
.map_err(|_| ServiceErrors::DatabaseConnectionLost)?;
diesel::delete(issue_assignees.filter(issue_id.eq(msg.issue_id))) diesel::delete(issue_assignees.filter(issue_id.eq(msg.issue_id)))
.execute(conn) .execute(conn)
@ -231,16 +234,16 @@ impl Handler<CreateIssue> for DbExecutor {
use crate::schema::issue_assignees::dsl; use crate::schema::issue_assignees::dsl;
use crate::schema::issues::dsl::issues; use crate::schema::issues::dsl::issues;
let conn = &self let conn = db_pool!(self);
.pool
.get()
.map_err(|_| ServiceErrors::DatabaseConnectionLost)?;
let list_position = issues let list_position = issues
// .filter(issue_status_id.eq(IssueStatus::Backlog)) // .filter(issue_status_id.eq(IssueStatus::Backlog))
.select(sql("max(list_position) + 1")) .select(sql("COALESCE(max(list_position), 0) + 1"))
.get_result::<i32>(conn) .get_result::<i32>(conn)
.map_err(|_| ServiceErrors::DatabaseConnectionLost)?; .map_err(|e| {
error!("resolve new issue position failed {}", e);
ServiceErrors::DatabaseConnectionLost
})?;
info!("{:?}", msg.issue_type); info!("{:?}", msg.issue_type);
info!("msg.issue_status_id {:?}", msg.issue_status_id); info!("msg.issue_status_id {:?}", msg.issue_status_id);
@ -252,9 +255,12 @@ impl Handler<CreateIssue> for DbExecutor {
}, },
ctx, ctx,
) )
.map_err(|_| ServiceErrors::DatabaseConnectionLost)? .map_err(|e| {
error!("{:?}", e);
ServiceErrors::Error(WsError::FailedToFetchIssueStatuses)
})?
.get(0) .get(0)
.ok_or_else(|| ServiceErrors::DatabaseConnectionLost)? .ok_or_else(|| ServiceErrors::Error(WsError::NoIssueStatuses))?
.id .id
} else { } else {
msg.issue_status_id msg.issue_status_id
@ -302,7 +308,10 @@ impl Handler<CreateIssue> for DbExecutor {
diesel::insert_into(dsl::issue_assignees) diesel::insert_into(dsl::issue_assignees)
.values(values) .values(values)
.execute(conn) .execute(conn)
.map_err(|_| ServiceErrors::DatabaseConnectionLost)?; .map_err(|e| {
error!("{:?}", e);
ServiceErrors::DatabaseConnectionLost
})?;
Ok(issue) Ok(issue)
} }

View File

@ -8,7 +8,9 @@ use crate::{
users::{FindUser, LookupUser}, users::{FindUser, LookupUser},
DbExecutor, DbExecutor,
}, },
db_pool,
errors::ServiceErrors, errors::ServiceErrors,
q,
}; };
#[derive(Debug)] #[derive(Debug)]
@ -26,19 +28,14 @@ impl Handler<LoadMessages> for DbExecutor {
fn handle(&mut self, msg: LoadMessages, _ctx: &mut Self::Context) -> Self::Result { fn handle(&mut self, msg: LoadMessages, _ctx: &mut Self::Context) -> Self::Result {
use crate::schema::messages::dsl::*; use crate::schema::messages::dsl::*;
let conn = &self let conn = db_pool!(self);
.pool
.get()
.map_err(|_| ServiceErrors::DatabaseConnectionLost)?;
let query = messages.filter(receiver_id.eq(msg.user_id)); q!(messages.filter(receiver_id.eq(msg.user_id)))
debug!(
"{}",
diesel::debug_query::<diesel::pg::Pg, _>(&query).to_string()
);
query
.load(conn) .load(conn)
.map_err(|_| ServiceErrors::DatabaseQueryFailed("load user messages".to_string())) .map_err(|e| {
error!("{:?}", e);
ServiceErrors::DatabaseQueryFailed("load user messages".to_string())
})
} }
} }
@ -58,20 +55,18 @@ impl Handler<MarkMessageSeen> for DbExecutor {
fn handle(&mut self, msg: MarkMessageSeen, _ctx: &mut Self::Context) -> Self::Result { fn handle(&mut self, msg: MarkMessageSeen, _ctx: &mut Self::Context) -> Self::Result {
use crate::schema::messages::dsl::*; use crate::schema::messages::dsl::*;
let conn = &self let conn = db_pool!(self);
.pool
.get()
.map_err(|_| ServiceErrors::DatabaseConnectionLost)?;
let query = diesel::delete( let size = q!(diesel::delete(
messages messages
.find(msg.message_id) .find(msg.message_id)
.filter(receiver_id.eq(msg.user_id)), .filter(receiver_id.eq(msg.user_id)),
); ))
debug!("{}", diesel::debug_query::<diesel::pg::Pg, _>(&query)); .execute(conn)
let size = query .map_err(|e| {
.execute(conn) error!("{:?}", e);
.map_err(|_| ServiceErrors::DatabaseQueryFailed("load user messages".to_string()))?; ServiceErrors::DatabaseQueryFailed("load user messages".to_string())
})?;
if size > 0 { if size > 0 {
Ok(msg.message_id) Ok(msg.message_id)
@ -110,10 +105,7 @@ impl Handler<CreateMessage> for DbExecutor {
fn handle(&mut self, msg: CreateMessage, ctx: &mut Self::Context) -> Self::Result { fn handle(&mut self, msg: CreateMessage, ctx: &mut Self::Context) -> Self::Result {
use crate::schema::messages::dsl::*; use crate::schema::messages::dsl::*;
let conn = &self let conn = db_pool!(self);
.pool
.get()
.map_err(|_| ServiceErrors::DatabaseConnectionLost)?;
let user: User = match { let user: User = match {
match msg.receiver { match msg.receiver {
@ -143,9 +135,10 @@ impl Handler<CreateMessage> for DbExecutor {
"{}", "{}",
diesel::debug_query::<diesel::pg::Pg, _>(&query).to_string() diesel::debug_query::<diesel::pg::Pg, _>(&query).to_string()
); );
query query.get_result(conn).map_err(|e| {
.get_result(conn) error!("{:?}", e);
.map_err(|_| ServiceErrors::DatabaseQueryFailed("create message failed".to_string())) ServiceErrors::DatabaseQueryFailed("create message failed".to_string())
})
} }
} }
@ -165,22 +158,17 @@ impl Handler<LookupMessagesByToken> for DbExecutor {
fn handle(&mut self, msg: LookupMessagesByToken, _ctx: &mut Self::Context) -> Self::Result { fn handle(&mut self, msg: LookupMessagesByToken, _ctx: &mut Self::Context) -> Self::Result {
use crate::schema::messages::dsl::*; use crate::schema::messages::dsl::*;
let conn = &self let conn = db_pool!(self);
.pool
.get()
.map_err(|_| ServiceErrors::DatabaseConnectionLost)?;
let query = messages.filter( q!(messages.filter(
hyper_link hyper_link
.eq(format!("#{}", msg.token)) .eq(format!("#{}", msg.token))
.and(receiver_id.eq(msg.user_id)), .and(receiver_id.eq(msg.user_id)),
); ))
debug!( .load(conn)
"{}", .map_err(|e| {
diesel::debug_query::<diesel::pg::Pg, _>(&query).to_string() error!("{:?}", e);
); ServiceErrors::DatabaseQueryFailed("create message failed".to_string())
query })
.load(conn)
.map_err(|_| ServiceErrors::DatabaseQueryFailed("create message failed".to_string()))
} }
} }

View File

@ -5,6 +5,8 @@ use diesel::pg::PgConnection;
use diesel::r2d2::{self, ConnectionManager}; use diesel::r2d2::{self, ConnectionManager};
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use crate::errors::ServiceErrors;
pub mod authorize_user; pub mod authorize_user;
pub mod comments; pub mod comments;
pub mod epics; pub mod epics;
@ -105,3 +107,68 @@ impl Configuration {
"db.test.toml" "db.test.toml"
} }
} }
#[macro_export]
macro_rules! db_pool {
($self: expr) => {
&$self.pool.get().map_err(|e| {
error!("{:?}", e);
ServiceErrors::DatabaseConnectionLost
})?
};
}
#[macro_export]
macro_rules! q {
($q: expr) => {{
let q = $q;
debug!(
"{}",
diesel::debug_query::<diesel::pg::Pg, _>(&q).to_string()
);
q
}};
}
pub struct Guard<'l> {
conn: &'l crate::db::DbPooledConn,
tm: &'l diesel::connection::AnsiTransactionManager,
}
impl<'l> Guard<'l> {
pub fn new(conn: &'l DbPooledConn) -> Result<Self, ServiceErrors> {
use diesel::{connection::TransactionManager, prelude::*};
let tm = conn.transaction_manager();
tm.begin_transaction(conn).map_err(|e| {
log::error!("{:?}", e);
ServiceErrors::DatabaseConnectionLost
})?;
Ok(Self { conn, tm })
}
pub fn run<R, F: FnOnce(&Guard) -> Result<R, ServiceErrors>>(
&self,
f: F,
) -> Result<R, ServiceErrors> {
use diesel::connection::TransactionManager;
let r = f(self);
match r {
Ok(r) => {
self.tm.commit_transaction(self.conn).map_err(|e| {
log::error!("{:?}", e);
ServiceErrors::DatabaseConnectionLost
})?;
Ok(r)
}
Err(e) => {
log::error!("{:?}", e);
self.tm.rollback_transaction(self.conn).map_err(|e| {
log::error!("{:?}", e);
ServiceErrors::DatabaseConnectionLost
})?;
Err(e)
}
}
}
}

View File

@ -1,19 +1,30 @@
use actix::{Handler, Message}; use actix::{Handler, Message};
use diesel::pg::Pg;
use diesel::prelude::*; use diesel::prelude::*;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use jirs_data::{NameString, Project, ProjectCategory, ProjectId, TimeTracking, UserId}; use jirs_data::{NameString, Project, ProjectCategory, ProjectId, TimeTracking, UserId};
use crate::db::DbExecutor; use crate::db::DbPooledConn;
use crate::errors::ServiceErrors; use crate::{db::DbExecutor, db_pool, errors::ServiceErrors, q, schema::projects::all_columns};
use crate::schema::projects::all_columns;
#[derive(Serialize, Deserialize)] #[derive(Serialize, Deserialize)]
pub struct LoadCurrentProject { pub struct LoadCurrentProject {
pub project_id: ProjectId, pub project_id: ProjectId,
} }
impl LoadCurrentProject {
pub fn execute(self, conn: &DbPooledConn) -> Result<Project, ServiceErrors> {
use crate::schema::projects::dsl::projects;
q!(projects.find(self.project_id))
.first::<Project>(conn)
.map_err(|e| {
error!("{:?}", e);
ServiceErrors::RecordNotFound("Project".to_string())
})
}
}
impl Message for LoadCurrentProject { impl Message for LoadCurrentProject {
type Result = Result<Project, ServiceErrors>; type Result = Result<Project, ServiceErrors>;
} }
@ -22,26 +33,12 @@ impl Handler<LoadCurrentProject> for DbExecutor {
type Result = Result<Project, ServiceErrors>; type Result = Result<Project, ServiceErrors>;
fn handle(&mut self, msg: LoadCurrentProject, _ctx: &mut Self::Context) -> Self::Result { fn handle(&mut self, msg: LoadCurrentProject, _ctx: &mut Self::Context) -> Self::Result {
use crate::schema::projects::dsl::projects; let conn = db_pool!(self);
let conn = &self
.pool
.get()
.map_err(|_| ServiceErrors::DatabaseConnectionLost)?;
let query = projects.find(msg.project_id); msg.execute(conn)
debug!(
"{}",
diesel::debug_query::<diesel::pg::Pg, _>(&query).to_string()
);
query
.first::<Project>(conn)
.map_err(|_| ServiceErrors::RecordNotFound("Project".to_string()))
} }
} }
#[derive(Serialize, Deserialize)]
pub struct CreateProject { pub struct CreateProject {
pub name: NameString, pub name: NameString,
pub url: Option<String>, pub url: Option<String>,
@ -50,6 +47,38 @@ pub struct CreateProject {
pub time_tracking: Option<TimeTracking>, pub time_tracking: Option<TimeTracking>,
} }
impl CreateProject {
pub fn execute(self, conn: &DbPooledConn) -> Result<Project, ServiceErrors> {
use crate::schema::projects::dsl::*;
crate::db::Guard::new(conn)?.run(|_guard| {
let p = q!(diesel::insert_into(projects)
.values((
name.eq(self.name),
self.url.map(|v| url.eq(v)),
self.description.map(|v| description.eq(v)),
self.category.map(|v| category.eq(v)),
self.time_tracking.map(|v| time_tracking.eq(v)),
))
.returning(all_columns))
.get_result::<Project>(conn)
.map_err(|e| {
error!("{:?}", e);
ServiceErrors::DatabaseQueryFailed(format!("{}", e))
})?;
crate::db::issue_statuses::CreateIssueStatus {
project_id: p.id,
position: 0,
name: "TODO".to_string(),
}
.execute(conn)?;
Ok(p)
})
}
}
impl Message for CreateProject { impl Message for CreateProject {
type Result = Result<Project, ServiceErrors>; type Result = Result<Project, ServiceErrors>;
} }
@ -58,29 +87,12 @@ impl Handler<CreateProject> for DbExecutor {
type Result = Result<Project, ServiceErrors>; type Result = Result<Project, ServiceErrors>;
fn handle(&mut self, msg: CreateProject, _ctx: &mut Self::Context) -> Self::Result { fn handle(&mut self, msg: CreateProject, _ctx: &mut Self::Context) -> Self::Result {
use crate::schema::projects::dsl::*; let conn = db_pool!(self);
let conn = &self
.pool
.get()
.map_err(|_| ServiceErrors::DatabaseConnectionLost)?;
let query = diesel::insert_into(projects) msg.execute(conn)
.values((
name.eq(msg.name),
msg.url.map(|v| url.eq(v)),
msg.description.map(|v| description.eq(v)),
msg.category.map(|v| category.eq(v)),
msg.time_tracking.map(|v| time_tracking.eq(v)),
))
.returning(all_columns);
debug!("{}", diesel::debug_query::<Pg, _>(&query));
query
.get_result::<Project>(conn)
.map_err(|e| ServiceErrors::DatabaseQueryFailed(format!("{}", e)))
} }
} }
#[derive(Serialize, Deserialize)]
pub struct UpdateProject { pub struct UpdateProject {
pub project_id: ProjectId, pub project_id: ProjectId,
pub name: Option<NameString>, pub name: Option<NameString>,
@ -90,6 +102,27 @@ pub struct UpdateProject {
pub time_tracking: Option<TimeTracking>, pub time_tracking: Option<TimeTracking>,
} }
impl UpdateProject {
pub fn execute(self, conn: &DbPooledConn) -> Result<Project, ServiceErrors> {
use crate::schema::projects::dsl::*;
q!(diesel::update(projects.find(self.project_id)).set((
self.name.map(|v| name.eq(v)),
self.url.map(|v| url.eq(v)),
self.description.map(|v| description.eq(v)),
self.category.map(|v| category.eq(v)),
self.time_tracking.map(|v| time_tracking.eq(v)),
)))
.execute(conn)
.map_err(|e| ServiceErrors::DatabaseQueryFailed(format!("{}", e)))?;
LoadCurrentProject {
project_id: self.project_id,
}
.execute(conn)
}
}
impl Message for UpdateProject { impl Message for UpdateProject {
type Result = Result<Project, ServiceErrors>; type Result = Result<Project, ServiceErrors>;
} }
@ -98,29 +131,9 @@ impl Handler<UpdateProject> for DbExecutor {
type Result = Result<Project, ServiceErrors>; type Result = Result<Project, ServiceErrors>;
fn handle(&mut self, msg: UpdateProject, _ctx: &mut Self::Context) -> Self::Result { fn handle(&mut self, msg: UpdateProject, _ctx: &mut Self::Context) -> Self::Result {
use crate::schema::projects::dsl::*; let conn = db_pool!(self);
let conn = &self
.pool
.get()
.map_err(|_| ServiceErrors::DatabaseConnectionLost)?;
let update_query = diesel::update(projects.find(msg.project_id)).set(( msg.execute(conn)
msg.name.map(|v| name.eq(v)),
msg.url.map(|v| url.eq(v)),
msg.description.map(|v| description.eq(v)),
msg.category.map(|v| category.eq(v)),
msg.time_tracking.map(|v| time_tracking.eq(v)),
));
debug!("{}", diesel::debug_query::<Pg, _>(&update_query));
update_query
.execute(conn)
.map_err(|e| ServiceErrors::DatabaseQueryFailed(format!("{}", e)))?;
let project_query = projects.find(msg.project_id);
debug!("{}", diesel::debug_query::<Pg, _>(&project_query));
project_query
.first::<Project>(conn)
.map_err(|_| ServiceErrors::RecordNotFound("Project".to_string()))
} }
} }
@ -128,6 +141,24 @@ pub struct LoadProjects {
pub user_id: UserId, pub user_id: UserId,
} }
impl LoadProjects {
pub fn execute(self, conn: &DbPooledConn) -> Result<Vec<Project>, ServiceErrors> {
use crate::schema::projects::dsl::*;
use crate::schema::user_projects::dsl::{project_id, user_id, user_projects};
q!(projects
.inner_join(user_projects.on(project_id.eq(id)))
.filter(user_id.eq(self.user_id))
.distinct_on(id)
.select(all_columns))
.load::<Project>(conn)
.map_err(|e| {
error!("{:?}", e);
ServiceErrors::RecordNotFound("Project".to_string())
})
}
}
impl Message for LoadProjects { impl Message for LoadProjects {
type Result = Result<Vec<Project>, ServiceErrors>; type Result = Result<Vec<Project>, ServiceErrors>;
} }
@ -136,22 +167,8 @@ impl Handler<LoadProjects> for DbExecutor {
type Result = Result<Vec<Project>, ServiceErrors>; type Result = Result<Vec<Project>, ServiceErrors>;
fn handle(&mut self, msg: LoadProjects, _ctx: &mut Self::Context) -> Self::Result { fn handle(&mut self, msg: LoadProjects, _ctx: &mut Self::Context) -> Self::Result {
use crate::schema::projects::dsl::*; let conn = db_pool!(self);
use crate::schema::user_projects::dsl::{project_id, user_id, user_projects};
let conn = &self msg.execute(conn)
.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))
.distinct_on(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

@ -1,19 +1,73 @@
use actix::{Handler, Message}; use actix::{Handler, Message};
use diesel::prelude::*; use diesel::prelude::*;
use serde::{Deserialize, Serialize};
use uuid::Uuid; use uuid::Uuid;
use jirs_data::msg::WsError;
use jirs_data::{Token, UserId}; use jirs_data::{Token, UserId};
use crate::db::DbExecutor; use crate::{
use crate::errors::ServiceErrors; db::{DbExecutor, DbPooledConn},
use crate::models::TokenForm; db_pool,
errors::ServiceErrors,
q,
};
pub struct FindUserId {
pub user_id: UserId,
}
impl FindUserId {
pub fn execute(self, conn: &DbPooledConn) -> Result<Token, ServiceErrors> {
use crate::schema::tokens::dsl::*;
q!(tokens.filter(user_id.eq(self.user_id)).order_by(id.desc()))
.first(conn)
.map_err(|e| {
error!("{:?}", e);
ServiceErrors::Error(WsError::NoBindToken)
})
}
}
impl Message for FindUserId {
type Result = Result<Token, ServiceErrors>;
}
impl Handler<FindUserId> for DbExecutor {
type Result = Result<Token, ServiceErrors>;
fn handle(&mut self, msg: FindUserId, _ctx: &mut Self::Context) -> Self::Result {
let conn = db_pool!(self);
msg.execute(conn)
}
}
#[derive(Serialize, Deserialize, Debug)]
pub struct FindBindToken { pub struct FindBindToken {
pub token: Uuid, pub token: Uuid,
} }
impl FindBindToken {
pub fn execute(self, conn: &DbPooledConn) -> Result<Token, ServiceErrors> {
use crate::schema::tokens::dsl::{bind_token, tokens};
let token: Token = q!(tokens.filter(bind_token.eq(Some(self.token))))
.first(conn)
.map_err(|e| {
error!("{:?}", e);
ServiceErrors::Error(WsError::BindTokenNotExists)
})?;
q!(diesel::update(tokens.find(token.id)).set(bind_token.eq(None as Option<Uuid>)))
.execute(conn)
.map_err(|e| {
error!("{:?}", e);
ServiceErrors::Error(WsError::FailedToDisableBindToken)
})?;
Ok(token)
}
}
impl Message for FindBindToken { impl Message for FindBindToken {
type Result = Result<Token, ServiceErrors>; type Result = Result<Token, ServiceErrors>;
} }
@ -22,32 +76,63 @@ impl Handler<FindBindToken> for DbExecutor {
type Result = Result<Token, ServiceErrors>; type Result = Result<Token, ServiceErrors>;
fn handle(&mut self, msg: FindBindToken, _: &mut Self::Context) -> Self::Result { fn handle(&mut self, msg: FindBindToken, _: &mut Self::Context) -> Self::Result {
use crate::schema::tokens::dsl::{bind_token, tokens}; let conn = db_pool!(self);
let conn = &self msg.execute(conn)
.pool }
.get() }
.map_err(|_| ServiceErrors::DatabaseConnectionLost)?;
pub struct FindAccessToken {
let token: Token = tokens pub token: Uuid,
.filter(bind_token.eq(Some(msg.token))) }
.first(conn)
.map_err(|_e| ServiceErrors::RecordNotFound(format!("token for {}", msg.token)))?; impl FindAccessToken {
pub fn execute(self, conn: &DbPooledConn) -> Result<Token, ServiceErrors> {
let erase_value: Option<Uuid> = None; use crate::schema::tokens::dsl::{access_token, tokens};
diesel::update(tokens.find(token.id))
.set(bind_token.eq(erase_value)) q!(tokens.filter(access_token.eq(self.token)))
.execute(conn) .first(conn)
.map_err(|_| ServiceErrors::DatabaseConnectionLost)?; .map_err(|e| {
error!("{:?}", e);
Ok(token) ServiceErrors::Error(WsError::AccessTokenNotExists)
})
}
}
impl Message for FindAccessToken {
type Result = Result<Token, ServiceErrors>;
}
impl Handler<FindAccessToken> for DbExecutor {
type Result = Result<Token, ServiceErrors>;
fn handle(&mut self, msg: FindAccessToken, _: &mut Self::Context) -> Self::Result {
let conn = db_pool!(self);
msg.execute(conn)
} }
} }
#[derive(Serialize, Deserialize, Debug)]
pub struct CreateBindToken { pub struct CreateBindToken {
pub user_id: UserId, pub user_id: UserId,
} }
impl CreateBindToken {
pub fn execute(self, conn: &DbPooledConn) -> Result<Token, ServiceErrors> {
use crate::schema::tokens::dsl::*;
q!(diesel::insert_into(tokens).values((
user_id.eq(self.user_id),
access_token.eq(Uuid::new_v4()),
refresh_token.eq(Uuid::new_v4()),
bind_token.eq(Some(Uuid::new_v4())),
)))
.get_result(conn)
.map_err(|e| {
error!("{:?}", e);
ServiceErrors::Error(WsError::FailedToCreateBindToken)
})
}
}
impl Message for CreateBindToken { impl Message for CreateBindToken {
type Result = Result<Token, ServiceErrors>; type Result = Result<Token, ServiceErrors>;
} }
@ -56,26 +141,8 @@ impl Handler<CreateBindToken> for DbExecutor {
type Result = Result<Token, ServiceErrors>; type Result = Result<Token, ServiceErrors>;
fn handle(&mut self, msg: CreateBindToken, _: &mut Self::Context) -> Self::Result { fn handle(&mut self, msg: CreateBindToken, _: &mut Self::Context) -> Self::Result {
use crate::schema::tokens::dsl::tokens; let conn = db_pool!(self);
let conn = &self
.pool
.get()
.map_err(|_| ServiceErrors::DatabaseConnectionLost)?;
let access_token = Uuid::new_v4(); msg.execute(conn)
let refresh_token = Uuid::new_v4();
let bind_token = Some(Uuid::new_v4());
let form = TokenForm {
user_id: msg.user_id,
access_token,
refresh_token,
bind_token,
};
diesel::insert_into(tokens)
.values(form)
.get_result(conn)
.map_err(|_| ServiceErrors::RecordNotFound("issue comments".to_string()))
} }
} }

View File

@ -1,12 +1,15 @@
use actix::{Handler, Message}; use actix::{Handler, Message};
use diesel::connection::TransactionManager;
use diesel::pg::Pg;
use diesel::prelude::*; use diesel::prelude::*;
use jirs_data::msg::WsError;
use jirs_data::{ProjectId, UserId, UserProject, UserProjectId, UserRole}; use jirs_data::{ProjectId, UserId, UserProject, UserProjectId, UserRole};
use crate::db::DbExecutor; use crate::{
use crate::errors::ServiceErrors; db::{DbExecutor, DbPooledConn},
db_pool,
errors::ServiceErrors,
q,
};
pub struct CurrentUserProject { pub struct CurrentUserProject {
pub user_id: UserId, pub user_id: UserId,
@ -22,16 +25,14 @@ impl Handler<CurrentUserProject> for DbExecutor {
fn handle(&mut self, msg: CurrentUserProject, _: &mut Self::Context) -> Self::Result { fn handle(&mut self, msg: CurrentUserProject, _: &mut Self::Context) -> Self::Result {
use crate::schema::user_projects::dsl::*; use crate::schema::user_projects::dsl::*;
let conn = &self let conn = db_pool!(self);
.pool
.get()
.map_err(|_| ServiceErrors::DatabaseConnectionLost)?;
let user_query = user_projects.filter(user_id.eq(msg.user_id).and(is_current.eq(true))); q!(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) .first(conn)
.map_err(|_e| ServiceErrors::RecordNotFound(format!("user project {}", msg.user_id))) .map_err(|e| {
error!("{:?}", e);
ServiceErrors::RecordNotFound(format!("user project {}", msg.user_id))
})
} }
} }
@ -49,16 +50,14 @@ impl Handler<LoadUserProjects> for DbExecutor {
fn handle(&mut self, msg: LoadUserProjects, _ctx: &mut Self::Context) -> Self::Result { fn handle(&mut self, msg: LoadUserProjects, _ctx: &mut Self::Context) -> Self::Result {
use crate::schema::user_projects::dsl::*; use crate::schema::user_projects::dsl::*;
let conn = &self let conn = db_pool!(self);
.pool
.get()
.map_err(|_| ServiceErrors::DatabaseConnectionLost)?;
let user_query = user_projects.filter(user_id.eq(msg.user_id)); q!(user_projects.filter(user_id.eq(msg.user_id)))
debug!("{}", diesel::debug_query::<Pg, _>(&user_query));
user_query
.load(conn) .load(conn)
.map_err(|_e| ServiceErrors::RecordNotFound(format!("user project {}", msg.user_id))) .map_err(|e| {
error!("{:?}", e);
ServiceErrors::RecordNotFound(format!("user project {}", msg.user_id))
})
} }
} }
@ -67,6 +66,51 @@ pub struct ChangeCurrentUserProject {
pub id: UserProjectId, pub id: UserProjectId,
} }
impl ChangeCurrentUserProject {
pub fn execute(self, conn: &DbPooledConn) -> Result<UserProject, ServiceErrors> {
use crate::schema::user_projects::dsl::*;
crate::db::Guard::new(conn)?.run(|_guard| {
let mut user_project: UserProject =
q!(user_projects.filter(id.eq(self.id).and(user_id.eq(self.user_id))))
.first(conn)
.map_err(|e| {
error!("{:?}", e);
ServiceErrors::RecordNotFound(format!("user project {}", self.user_id))
})?;
q!(diesel::update(user_projects)
.set(is_current.eq(false))
.filter(user_id.eq(self.user_id)))
.execute(conn)
.map(|_| ())
.map_err(|e| {
error!("{:?}", e);
ServiceErrors::DatabaseQueryFailed(format!(
"setting current flag to false while updating current project {}",
self.user_id
))
})?;
q!(diesel::update(user_projects)
.set(is_current.eq(true))
.filter(id.eq(self.id).and(user_id.eq(self.user_id))))
.execute(conn)
.map(|_| ())
.map_err(|e| {
error!("{:?}", e);
ServiceErrors::DatabaseQueryFailed(format!(
"set current flag on project while updating current project {}",
self.user_id
))
})?;
user_project.is_current = true;
Ok(user_project)
})
}
}
impl Message for ChangeCurrentUserProject { impl Message for ChangeCurrentUserProject {
type Result = Result<UserProject, ServiceErrors>; type Result = Result<UserProject, ServiceErrors>;
} }
@ -75,63 +119,8 @@ impl Handler<ChangeCurrentUserProject> for DbExecutor {
type Result = Result<UserProject, ServiceErrors>; type Result = Result<UserProject, ServiceErrors>;
fn handle(&mut self, msg: ChangeCurrentUserProject, _ctx: &mut Self::Context) -> Self::Result { fn handle(&mut self, msg: ChangeCurrentUserProject, _ctx: &mut Self::Context) -> Self::Result {
use crate::schema::user_projects::dsl::*; let conn = db_pool!(self);
msg.execute(conn)
let conn = &self
.pool
.get()
.map_err(|_| ServiceErrors::DatabaseConnectionLost)?;
let tm = conn.transaction_manager();
tm.begin_transaction(conn)
.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| match tm.rollback_transaction(conn) {
Err(_) => ServiceErrors::DatabaseConnectionLost,
_ => 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| match tm.rollback_transaction(conn) {
Err(_) => ServiceErrors::DatabaseConnectionLost,
_ => ServiceErrors::DatabaseQueryFailed(format!(
"setting current flag to false while updating current 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| match tm.rollback_transaction(conn) {
Err(_) => ServiceErrors::DatabaseConnectionLost,
_ => ServiceErrors::DatabaseQueryFailed(format!(
"set current flag on project while updating current project {}",
msg.user_id
)),
})?;
tm.commit_transaction(conn)
.map_err(|_| ServiceErrors::DatabaseConnectionLost)?;
user_project.is_current = true;
Ok(user_project)
} }
} }
@ -141,6 +130,42 @@ pub struct RemoveInvitedUser {
pub project_id: ProjectId, pub project_id: ProjectId,
} }
impl RemoveInvitedUser {
pub fn execute(self, conn: &DbPooledConn) -> Result<usize, ServiceErrors> {
use crate::schema::user_projects::dsl::*;
if self.invited_id == self.inviter_id {
return Err(ServiceErrors::Unauthorized);
}
q!(user_projects.filter(
user_id
.eq(self.inviter_id)
.and(project_id.eq(self.project_id))
.and(role.eq(UserRole::Owner)),
))
.first::<UserProject>(conn)
.map_err(|e| {
error!("{:?}", e);
ServiceErrors::Unauthorized
})?;
q!(diesel::delete(user_projects).filter(
user_id
.eq(self.invited_id)
.and(project_id.eq(self.project_id)),
))
.execute(conn)
.map_err(|e| {
error!("{:?}", e);
ServiceErrors::RecordNotFound(format!(
"user project user with id {} for project {}",
self.invited_id, self.project_id
))
})
}
}
impl Message for RemoveInvitedUser { impl Message for RemoveInvitedUser {
type Result = Result<(), ServiceErrors>; type Result = Result<(), ServiceErrors>;
} }
@ -149,46 +174,48 @@ impl Handler<RemoveInvitedUser> for DbExecutor {
type Result = Result<(), ServiceErrors>; type Result = Result<(), ServiceErrors>;
fn handle(&mut self, msg: RemoveInvitedUser, _ctx: &mut Self::Context) -> Self::Result { fn handle(&mut self, msg: RemoveInvitedUser, _ctx: &mut Self::Context) -> Self::Result {
use crate::schema::user_projects::dsl::*; let conn = db_pool!(self);
msg.execute(conn)?;
let conn = &self Ok(())
.pool }
.get() }
.map_err(|_| ServiceErrors::DatabaseConnectionLost)?;
pub struct CreateUserProject {
if msg.invited_id == msg.inviter_id { pub user_id: UserId,
return Err(ServiceErrors::Unauthorized); pub project_id: ProjectId,
} pub is_current: bool,
pub is_default: bool,
{ pub role: UserRole,
let owner = UserRole::Owner; }
let query = user_projects.filter(
user_id impl CreateUserProject {
.eq(msg.inviter_id) pub fn execute(self, conn: &DbPooledConn) -> Result<usize, ServiceErrors> {
.and(project_id.eq(msg.project_id)) use crate::schema::user_projects::dsl::*;
.and(role.eq(owner)), q!(diesel::insert_into(user_projects).values((
); user_id.eq(self.user_id),
debug!("{}", diesel::debug_query::<Pg, _>(&query)); project_id.eq(self.project_id),
query is_current.eq(self.is_current),
.first::<UserProject>(conn) is_default.eq(self.is_default),
.map_err(|_e| ServiceErrors::Unauthorized)?; role.eq(self.role),
} )))
.execute(conn)
{ .map_err(|e| {
let query = diesel::delete(user_projects).filter( error!("{:?}", e);
user_id ServiceErrors::Error(WsError::InvalidUserProject)
.eq(msg.invited_id) })
.and(project_id.eq(msg.project_id)), }
); }
debug!("{}", diesel::debug_query::<Pg, _>(&query));
query.execute(conn).map_err(|_e| { impl Message for CreateUserProject {
ServiceErrors::RecordNotFound(format!( type Result = Result<(), ServiceErrors>;
"user project user with id {} for project {}", }
msg.invited_id, msg.project_id
)) impl Handler<CreateUserProject> for DbExecutor {
})?; type Result = Result<(), ServiceErrors>;
}
fn handle(&mut self, msg: CreateUserProject, _ctx: &mut Self::Context) -> Self::Result {
let conn = db_pool!(self);
msg.execute(conn)?;
Ok(()) Ok(())
} }
} }

View File

@ -1,21 +1,34 @@
use actix::{Handler, Message}; use actix::{Handler, Message};
use diesel::connection::TransactionManager;
use diesel::pg::Pg;
use diesel::prelude::*; use diesel::prelude::*;
use serde::{Deserialize, Serialize}; use diesel::result::Error;
use jirs_data::{ProjectId, User, UserId}; use jirs_data::{msg::WsError, EmailString, ProjectId, User, UserId, UserRole, UsernameString};
use crate::db::projects::CreateProject; use crate::db::user_projects::CreateUserProject;
use crate::db::{DbExecutor, DbPooledConn}; use crate::{
use crate::errors::ServiceErrors; db::{projects::CreateProject, DbExecutor, DbPooledConn},
use crate::schema::users::all_columns; db_pool,
errors::ServiceErrors,
q,
schema::users::all_columns,
};
#[derive(Debug)] #[derive(Debug)]
pub struct FindUser { pub struct FindUser {
pub user_id: UserId, pub user_id: UserId,
} }
impl FindUser {
pub fn execute(self, conn: &DbPooledConn) -> Result<User, ServiceErrors> {
use crate::schema::users::dsl::*;
q!(users.find(self.user_id)).first(conn).map_err(|e| {
error!("{:?}", e);
ServiceErrors::Error(WsError::UserNotExists(self.user_id))
})
}
}
impl Message for FindUser { impl Message for FindUser {
type Result = Result<User, ServiceErrors>; type Result = Result<User, ServiceErrors>;
} }
@ -24,27 +37,32 @@ impl Handler<FindUser> for DbExecutor {
type Result = Result<User, ServiceErrors>; type Result = Result<User, ServiceErrors>;
fn handle(&mut self, msg: FindUser, _ctx: &mut Self::Context) -> Self::Result { fn handle(&mut self, msg: FindUser, _ctx: &mut Self::Context) -> Self::Result {
use crate::schema::users::dsl::*; let conn = db_pool!(self);
msg.execute(conn)
let conn = &self
.pool
.get()
.map_err(|_| ServiceErrors::DatabaseConnectionLost)?;
let query = users.find(msg.user_id);
debug!("{}", diesel::debug_query::<Pg, _>(&query));
query
.first(conn)
.map_err(|_| ServiceErrors::RecordNotFound(format!("user with id = {}", msg.user_id)))
} }
} }
#[derive(Debug)]
pub struct LookupUser { pub struct LookupUser {
pub name: String, pub name: String,
pub email: String, pub email: String,
} }
impl LookupUser {
pub fn execute(self, conn: &DbPooledConn) -> Result<User, ServiceErrors> {
use crate::schema::users::dsl::*;
q!(users
.distinct_on(id)
.filter(email.eq(self.email.as_str()))
.filter(name.eq(self.name.as_str())))
.first(conn)
.map_err(|e| {
error!("{:?}", e);
ServiceErrors::Error(WsError::NoMatchingPair(self.name, self.email))
})
}
}
impl Message for LookupUser { impl Message for LookupUser {
type Result = Result<User, ServiceErrors>; type Result = Result<User, ServiceErrors>;
} }
@ -53,29 +71,33 @@ impl Handler<LookupUser> for DbExecutor {
type Result = Result<User, ServiceErrors>; type Result = Result<User, ServiceErrors>;
fn handle(&mut self, msg: LookupUser, _ctx: &mut Self::Context) -> Self::Result { fn handle(&mut self, msg: LookupUser, _ctx: &mut Self::Context) -> Self::Result {
use crate::schema::users::dsl::*; let conn = db_pool!(self);
msg.execute(conn)
let conn = &self
.pool
.get()
.map_err(|_| ServiceErrors::DatabaseConnectionLost)?;
let users_query = users
.distinct_on(id)
.filter(email.eq(msg.email.as_str()))
.filter(name.eq(msg.name.as_str()));
debug!("{}", diesel::debug_query::<Pg, _>(&users_query));
users_query
.first(conn)
.map_err(|_| ServiceErrors::RecordNotFound(format!("user {} {}", msg.name, msg.email)))
} }
} }
#[derive(Debug)]
pub struct LoadProjectUsers { pub struct LoadProjectUsers {
pub project_id: i32, pub project_id: i32,
} }
impl LoadProjectUsers {
pub fn execute(self, conn: &DbPooledConn) -> Result<Vec<User>, ServiceErrors> {
use crate::schema::user_projects::dsl::{project_id, user_id, user_projects};
use crate::schema::users::dsl::*;
q!(users
.distinct_on(id)
.inner_join(user_projects.on(user_id.eq(id)))
.filter(project_id.eq(self.project_id))
.select(all_columns))
.load(conn)
.map_err(|e| {
error!("{:?}", e);
ServiceErrors::Error(WsError::FailedToLoadProjectUsers)
})
}
}
impl Message for LoadProjectUsers { impl Message for LoadProjectUsers {
type Result = Result<Vec<User>, ServiceErrors>; type Result = Result<Vec<User>, ServiceErrors>;
} }
@ -84,31 +106,33 @@ 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}; let conn = db_pool!(self);
use crate::schema::users::dsl::*; msg.execute(conn)
let conn = &self
.pool
.get()
.map_err(|_| ServiceErrors::DatabaseConnectionLost)?;
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));
users_query
.load(conn)
.map_err(|_| ServiceErrors::RecordNotFound("project users".to_string()))
} }
} }
#[derive(Debug)]
pub struct LoadIssueAssignees { pub struct LoadIssueAssignees {
pub issue_id: i32, pub issue_id: i32,
} }
impl LoadIssueAssignees {
pub fn execute(self, conn: &DbPooledConn) -> Result<Vec<User>, ServiceErrors> {
use crate::schema::issue_assignees::dsl::{issue_assignees, issue_id, user_id};
use crate::schema::users::dsl::*;
q!(users
.distinct_on(id)
.inner_join(issue_assignees.on(user_id.eq(id)))
.filter(issue_id.eq(self.issue_id))
.select(users::all_columns()))
.load(conn)
.map_err(|e| {
error!("{:?}", e);
ServiceErrors::Error(WsError::FailedToLoadAssignees)
})
}
}
impl Message for LoadIssueAssignees { impl Message for LoadIssueAssignees {
type Result = Result<Vec<User>, ServiceErrors>; type Result = Result<Vec<User>, ServiceErrors>;
} }
@ -117,31 +141,110 @@ impl Handler<LoadIssueAssignees> for DbExecutor {
type Result = Result<Vec<User>, ServiceErrors>; type Result = Result<Vec<User>, ServiceErrors>;
fn handle(&mut self, msg: LoadIssueAssignees, _ctx: &mut Self::Context) -> Self::Result { fn handle(&mut self, msg: LoadIssueAssignees, _ctx: &mut Self::Context) -> Self::Result {
use crate::schema::issue_assignees::dsl::{issue_assignees, issue_id, user_id}; let conn = db_pool!(self);
use crate::schema::users::dsl::*; msg.execute(conn)
}
let conn = &self }
.pool
.get() pub struct CreateUser {
.map_err(|_| ServiceErrors::DatabaseConnectionLost)?; pub name: UsernameString,
pub email: EmailString,
let users_query = users }
.distinct_on(id)
.inner_join(issue_assignees.on(user_id.eq(id))) impl CreateUser {
.filter(issue_id.eq(msg.issue_id)) pub fn execute(self, conn: &DbPooledConn) -> Result<User, ServiceErrors> {
.select(users::all_columns()); use crate::schema::users::dsl::*;
debug!("{}", diesel::debug_query::<Pg, _>(&users_query));
users_query q!(diesel::insert_into(users)
.load(conn) .values((name.eq(self.name.as_str()), email.eq(self.email.as_str()))))
.map_err(|_| ServiceErrors::RecordNotFound("issue users".to_string())) .get_result(conn)
.map_err(|e| {
error!("{:?}", e);
let ws = match e {
Error::InvalidCString(_) => WsError::InvalidPair(self.name, self.email),
Error::DatabaseError(diesel::result::DatabaseErrorKind::UniqueViolation, _) => {
WsError::TakenPair(self.name, self.email)
}
Error::DatabaseError(_, _) => WsError::InvalidPair(self.name, self.email),
Error::NotFound => WsError::InvalidPair(self.name, self.email),
Error::QueryBuilderError(_) => WsError::InvalidPair(self.name, self.email),
Error::DeserializationError(_) => WsError::InvalidPair(self.name, self.email),
Error::SerializationError(_) => WsError::InvalidPair(self.name, self.email),
Error::RollbackTransaction => WsError::InvalidPair(self.name, self.email),
Error::AlreadyInTransaction => WsError::InvalidPair(self.name, self.email),
Error::__Nonexhaustive => WsError::InvalidPair(self.name, self.email),
};
ServiceErrors::Error(ws)
})
}
}
impl Message for CreateUser {
type Result = Result<User, ServiceErrors>;
}
impl Handler<CreateUser> for DbExecutor {
type Result = Result<User, ServiceErrors>;
fn handle(&mut self, msg: CreateUser, _ctx: &mut Self::Context) -> Self::Result {
let conn = db_pool!(self);
msg.execute(conn)
} }
} }
#[derive(Debug)]
pub struct Register { pub struct Register {
pub name: String, pub name: UsernameString,
pub email: String, pub email: EmailString,
pub project_id: Option<ProjectId>, pub project_id: Option<ProjectId>,
pub role: UserRole,
}
impl Register {
pub fn execute(self, conn: &DbPooledConn) -> Result<(), ServiceErrors> {
let Register {
name: given_name,
email: given_email,
project_id: given_project_id,
role: given_role,
} = self;
crate::db::Guard::new(conn)?.run(|_guard| {
if count_matching_users(given_name.as_str(), given_email.as_str(), conn) > 0 {
return Err(ServiceErrors::Error(WsError::InvalidLoginPair));
}
let current_project_id: ProjectId = match given_project_id {
Some(current_project_id) => current_project_id,
_ => {
CreateProject {
name: "initial".to_string(),
url: None,
description: None,
category: None,
time_tracking: None,
}
.execute(conn)?
.id
}
};
let user: User = CreateUser {
name: given_name,
email: given_email,
}
.execute(conn)?;
CreateUserProject {
user_id: user.id,
project_id: current_project_id,
is_current: true,
is_default: true,
role: given_role,
}
.execute(conn)?;
Ok(())
})
}
} }
impl Message for Register { impl Message for Register {
@ -151,74 +254,12 @@ impl Message for Register {
impl Handler<Register> for DbExecutor { impl Handler<Register> for DbExecutor {
type Result = Result<(), ServiceErrors>; type Result = Result<(), ServiceErrors>;
fn handle(&mut self, msg: Register, ctx: &mut Self::Context) -> Self::Result { fn handle(&mut self, msg: Register, _ctx: &mut Self::Context) -> Self::Result {
use crate::schema::users::dsl::*; let conn = db_pool!(self);
msg.execute(conn)
let conn = &self
.pool
.get()
.map_err(|_| ServiceErrors::DatabaseConnectionLost)?;
let tm = conn.transaction_manager();
tm.begin_transaction(conn)
.map_err(|_| ServiceErrors::DatabaseConnectionLost)?;
let matching = count_matching_users(msg.name.as_str(), msg.email.as_str(), conn);
if matching > 0 {
return Err(ServiceErrors::RegisterCollision);
}
let current_project_id: ProjectId = match msg.project_id.as_ref().cloned() {
Some(current_project_id) => current_project_id,
_ => {
self.handle(
CreateProject {
name: "initial".to_string(),
url: None,
description: None,
category: None,
time_tracking: None,
},
ctx,
)?
.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::<Pg, _>(&insert_user_query));
insert_user_query
.get_result(conn)
.map_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(current_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)?;
}
tm.commit_transaction(conn)
.map_err(|_| ServiceErrors::DatabaseConnectionLost)?;
Ok(())
} }
} }
#[derive(Serialize, Deserialize, Debug)]
pub struct LoadInvitedUsers { pub struct LoadInvitedUsers {
pub user_id: UserId, pub user_id: UserId,
} }
@ -234,35 +275,32 @@ impl Handler<LoadInvitedUsers> for DbExecutor {
use crate::schema::invitations::dsl::{email as i_email, invitations, invited_by_id}; use crate::schema::invitations::dsl::{email as i_email, invitations, invited_by_id};
use crate::schema::users::dsl::{email as u_email, users}; use crate::schema::users::dsl::{email as u_email, users};
let conn = &self let conn = db_pool!(self);
.pool
.get()
.map_err(|_| ServiceErrors::DatabaseConnectionLost)?;
let query = users q!(users
.inner_join(invitations.on(i_email.eq(u_email))) .inner_join(invitations.on(i_email.eq(u_email)))
.filter(invited_by_id.eq(msg.user_id)) .filter(invited_by_id.eq(msg.user_id))
.select(users::all_columns()); .select(users::all_columns()))
debug!("{}", diesel::debug_query::<Pg, _>(&query)); .load(conn)
query .map_err(|e| {
.load(conn) error!("{:?}", e);
.map_err(|e| ServiceErrors::DatabaseQueryFailed(format!("{}", e))) ServiceErrors::Error(WsError::FailedToLoadInvitedUsers)
})
} }
} }
fn count_matching_users(name: &str, email: &str, conn: &DbPooledConn) -> i64 { fn count_matching_users(name: &str, email: &str, conn: &DbPooledConn) -> i64 {
use crate::schema::users::dsl; use crate::schema::users::dsl;
let query = dsl::users q!(dsl::users
.filter(dsl::email.eq(email).and(dsl::name.ne(name))) .filter(dsl::email.eq(email).and(dsl::name.ne(name)))
.or_filter(dsl::email.ne(email).and(dsl::name.eq(name))) .or_filter(dsl::email.ne(email).and(dsl::name.eq(name)))
.or_filter(dsl::email.eq(email).and(dsl::name.eq(name))) .or_filter(dsl::email.eq(email).and(dsl::name.eq(name)))
.count(); .count())
info!("{}", diesel::debug_query::<diesel::pg::Pg, _>(&query)); .get_result::<i64>(conn)
query.get_result::<i64>(conn).unwrap_or(1) .unwrap_or(1)
} }
#[derive(Serialize, Deserialize, Debug)]
pub struct UpdateAvatarUrl { pub struct UpdateAvatarUrl {
pub user_id: UserId, pub user_id: UserId,
pub avatar_url: Option<String>, pub avatar_url: Option<String>,
@ -278,27 +316,24 @@ impl Handler<UpdateAvatarUrl> for DbExecutor {
fn handle(&mut self, msg: UpdateAvatarUrl, _ctx: &mut Self::Context) -> Self::Result { fn handle(&mut self, msg: UpdateAvatarUrl, _ctx: &mut Self::Context) -> Self::Result {
use crate::schema::users::dsl::{avatar_url, id, users}; use crate::schema::users::dsl::{avatar_url, id, users};
let conn = &self let conn = db_pool!(self);
.pool
.get()
.map_err(|_| ServiceErrors::DatabaseConnectionLost)?;
let update_query = diesel::update(users)
.set(avatar_url.eq(msg.avatar_url))
.filter(id.eq(msg.user_id));
debug!("{}", diesel::debug_query::<Pg, _>(&update_query));
update_query
.execute(conn)
.map_err(|e| ServiceErrors::DatabaseQueryFailed(format!("{}", e)))?;
let user_query = users.find(msg.user_id); q!(diesel::update(users)
debug!("{}", diesel::debug_query::<Pg, _>(&user_query)); .set(avatar_url.eq(msg.avatar_url))
user_query .filter(id.eq(msg.user_id)))
.first(conn) .execute(conn)
.map_err(|e| ServiceErrors::DatabaseQueryFailed(format!("{}", e))) .map_err(|e| {
error!("{:?}", e);
ServiceErrors::Error(WsError::FailedToChangeAvatar)
})?;
FindUser {
user_id: msg.user_id,
}
.execute(conn)
} }
} }
#[derive(Serialize, Deserialize, Debug)]
pub struct ProfileUpdate { pub struct ProfileUpdate {
pub user_id: UserId, pub user_id: UserId,
pub name: String, pub name: String,
@ -315,21 +350,15 @@ impl Handler<ProfileUpdate> for DbExecutor {
fn handle(&mut self, msg: ProfileUpdate, _ctx: &mut Self::Context) -> Self::Result { fn handle(&mut self, msg: ProfileUpdate, _ctx: &mut Self::Context) -> Self::Result {
use crate::schema::users::dsl::{email, id, name, users}; use crate::schema::users::dsl::{email, id, name, users};
let conn = &self let conn = db_pool!(self);
.pool
.get()
.map_err(|_| ServiceErrors::DatabaseConnectionLost)?;
let update_query = diesel::update(users)
.set((email.eq(msg.email), name.eq(msg.name)))
.filter(id.eq(msg.user_id));
debug!("{}", diesel::debug_query::<Pg, _>(&update_query));
update_query
.execute(conn)
.map_err(|e| ServiceErrors::DatabaseQueryFailed(format!("{}", e)))?;
let user_query = users.find(msg.user_id); q!(diesel::update(users)
debug!("{}", diesel::debug_query::<Pg, _>(&user_query)); .set((email.eq(msg.email), name.eq(msg.name)))
user_query .filter(id.eq(msg.user_id)))
.execute(conn)
.map_err(|e| ServiceErrors::DatabaseQueryFailed(format!("{}", e)))?;
q!(users.find(msg.user_id))
.first(conn) .first(conn)
.map_err(|e| ServiceErrors::DatabaseQueryFailed(format!("{}", e))) .map_err(|e| ServiceErrors::DatabaseQueryFailed(format!("{}", e)))
} }

View File

@ -1,6 +1,6 @@
use actix_web::HttpResponse; use actix_web::HttpResponse;
use jirs_data::ErrorResponse; use jirs_data::{msg::WsError, ErrorResponse};
const TOKEN_NOT_FOUND: &str = "Token not found"; const TOKEN_NOT_FOUND: &str = "Token not found";
const DATABASE_CONNECTION_FAILED: &str = "Database connection failed"; const DATABASE_CONNECTION_FAILED: &str = "Database connection failed";
@ -12,6 +12,7 @@ pub enum ServiceErrors {
DatabaseQueryFailed(String), DatabaseQueryFailed(String),
RecordNotFound(String), RecordNotFound(String),
RegisterCollision, RegisterCollision,
Error(WsError),
} }
impl ServiceErrors { impl ServiceErrors {
@ -44,6 +45,9 @@ impl Into<HttpResponse> for ServiceErrors {
ServiceErrors::RegisterCollision => HttpResponse::Unauthorized().json(ErrorResponse { ServiceErrors::RegisterCollision => HttpResponse::Unauthorized().json(ErrorResponse {
errors: vec!["Register collision".to_string()], errors: vec!["Register collision".to_string()],
}), }),
ServiceErrors::Error(error) => HttpResponse::BadRequest().json(ErrorResponse {
errors: vec![error.to_str().to_string()],
}),
} }
} }
} }

View File

@ -8,13 +8,15 @@ extern crate diesel;
extern crate log; extern crate log;
use actix::Actor; use actix::Actor;
use actix_cors::Cors; // use actix_cors::Cors;
#[cfg(feature = "local-storage")] #[cfg(feature = "local-storage")]
use actix_files as fs; use actix_files as fs;
use actix_web::{App, HttpServer}; use actix_web::{App, HttpServer};
use crate::ws::WsServer; use crate::ws::WsServer;
// use actix_web::http::Method;
pub mod db; pub mod db;
pub mod errors; pub mod errors;
pub mod mail; pub mod mail;
@ -53,7 +55,6 @@ async fn main() -> Result<(), String> {
HttpServer::new(move || { HttpServer::new(move || {
let app = App::new() let app = App::new()
.wrap(actix_web::middleware::Logger::default()) .wrap(actix_web::middleware::Logger::default())
.wrap(Cors::default())
.data(ws_server.clone()) .data(ws_server.clone())
.data(db_addr.clone()) .data(db_addr.clone())
.data(mail_addr.clone()) .data(mail_addr.clone())

View File

@ -9,14 +9,6 @@ use jirs_data::{
use crate::schema::*; use crate::schema::*;
#[derive(Debug, Serialize, Deserialize, Insertable)]
#[table_name = "comments"]
pub struct CommentForm {
pub body: String,
pub user_id: i32,
pub issue_id: i32,
}
#[derive(Debug, Serialize, Deserialize, Queryable)] #[derive(Debug, Serialize, Deserialize, Queryable)]
pub struct Issue { pub struct Issue {
pub id: i32, pub id: i32,

View File

@ -262,10 +262,7 @@ impl WebSocketActor {
self.current_user_project self.current_user_project
.as_ref() .as_ref()
.map(|u| u) .map(|u| u)
.ok_or_else(|| { .ok_or_else(|| WsMsg::AuthorizeExpired)
let _x = 1;
WsMsg::AuthorizeExpired
})
} }
fn load_user_project(&self) -> Result<UserProject, WsMsg> { fn load_user_project(&self) -> Result<UserProject, WsMsg> {
@ -273,11 +270,11 @@ impl WebSocketActor {
match block_on(self.db.send(CurrentUserProject { user_id })) { match block_on(self.db.send(CurrentUserProject { user_id })) {
Ok(Ok(user_project)) => Ok(user_project), Ok(Ok(user_project)) => Ok(user_project),
Ok(Err(e)) => { Ok(Err(e)) => {
error!("{:?}", e); error!("load_user_project encounter service error {:?}", e);
Err(WsMsg::AuthorizeExpired) Err(WsMsg::AuthorizeExpired)
} }
Err(e) => { Err(e) => {
error!("{}", e); error!("load_user_project encounter mailbox error {}", e);
Err(WsMsg::AuthorizeExpired) Err(WsMsg::AuthorizeExpired)
} }
} }

View File

@ -1,11 +1,11 @@
use futures::executor::block_on; use futures::executor::block_on;
use jirs_data::{UserId, UserProject, WsMsg}; use jirs_data::{UserId, UserProject, UserRole, WsMsg};
use crate::db; use crate::{
use crate::db::users::Register as DbRegister; db::{self, users::Register as DbRegister},
use crate::ws::auth::Authenticate; ws::{auth::Authenticate, WebSocketActor, WsHandler, WsResult},
use crate::ws::{WebSocketActor, WsHandler, WsResult}; };
pub struct LoadProjectUsers; pub struct LoadProjectUsers;
@ -41,6 +41,7 @@ impl WsHandler<Register> for WebSocketActor {
name: name.clone(), name: name.clone(),
email: email.clone(), email: email.clone(),
project_id: None, project_id: None,
role: UserRole::Owner,
})) { })) {
Ok(Ok(_)) => Some(WsMsg::SignUpSuccess), Ok(Ok(_)) => Some(WsMsg::SignUpSuccess),
Ok(Err(_)) => Some(WsMsg::SignUpPairTaken), Ok(Err(_)) => Some(WsMsg::SignUpPairTaken),