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]]
name = "serde"
version = "1.0.116"
version = "1.0.117"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "96fe57af81d28386a513cbc6858332abc6117cfdb5999647c6444b8f43a370a5"
checksum = "b88fa983de7720629c9387e9f517353ed404164b1e482c970a90c1a4aaf7dc1a"
dependencies = [
"serde_derive",
]
@ -3135,9 +3135,9 @@ dependencies = [
[[package]]
name = "serde_derive"
version = "1.0.116"
version = "1.0.117"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f630a6370fd8e457873b4bd2ffdae75408bc291ba72be773772a4c2a065d9ae8"
checksum = "cbd1ae72adb44aab48f325a02444a5fc079349a8d804c1fc922aed3f7454c74e"
dependencies = [
"proc-macro2",
"quote",
@ -3329,9 +3329,9 @@ checksum = "2d67a5a62ba6e01cb2192ff309324cb4875d0c451d55fe2319433abe7a05a8ee"
[[package]]
name = "syn"
version = "1.0.44"
version = "1.0.45"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e03e57e4fcbfe7749842d53e24ccb9aa12b7252dbe5e91d2acad31834c8b8fdd"
checksum = "ea9c5432ff16d6152371f808fb5a871cd67368171b09bb21b43df8e4a47a3556"
dependencies = [
"proc-macro2",
"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_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:
build:
dockerfile: ./jirs-server/Dockerfile
@ -57,3 +75,5 @@ services:
volumes:
assets:
build:
rs-target:

View File

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

View File

@ -1,6 +1,6 @@
use syntect::easy::HighlightLines;
use syntect::highlighting::{FontStyle, Style};
use syntect::parsing::SyntaxReference;
// use syntect::easy::HighlightLines;
// use syntect::highlighting::{FontStyle, Style};
// use syntect::parsing::SyntaxReference;
use wasm_bindgen::prelude::*;
#[wasm_bindgen(final, js_name = JirsCodeBuilder)]
@ -15,51 +15,52 @@ impl JirsCodeBuilder {
}
#[wasm_bindgen]
pub fn hi_code(&mut self, lang: &str, code: &str) -> String {
let syntax = {
match crate::hi::syntax_set::load().find_syntax_by_name(lang) {
Some(s) => s.clone(),
_ => {
return code.to_string();
}
}
};
let mut buffer: Vec<String> = Vec::with_capacity(code.lines().count() * 2);
for line in code.lines() {
self.hi(&syntax, line, &mut buffer);
buffer.push("<br />".to_string());
}
buffer.join("")
pub fn hi_code(&mut self, _lang: &str, code: &str) -> String {
// let syntax = {
// match crate::hi::syntax_set::load().find_syntax_by_name(lang) {
// Some(s) => s.clone(),
// _ => {
// return code.to_string();
// }
// }
// };
// let mut buffer: Vec<String> = Vec::with_capacity(code.lines().count() * 2);
// for line in code.lines() {
// self.hi(&syntax, line, &mut buffer);
// buffer.push("<br />".to_string());
// }
// buffer.join("")
code.to_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 tokens = { h.highlight(line, &crate::hi::syntax_set::load()) };
for (style, token) in tokens.into_iter() {
let Style {
foreground: f,
background: b,
font_style,
} = style;
let fs = if font_style == FontStyle::BOLD {
"font-weight: bold"
} else if font_style == FontStyle::ITALIC {
"font-style: italic"
} else if font_style == FontStyle::UNDERLINE {
"text-decoration: underline"
} else {
""
};
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>"#,
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,
fs = fs
));
}
}
// 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 tokens = { h.highlight(line, &crate::hi::syntax_set::load()) };
//
// for (style, token) in tokens.into_iter() {
// let Style {
// foreground: f,
// background: b,
// font_style,
// } = style;
// let fs = if font_style == FontStyle::BOLD {
// "font-weight: bold"
// } else if font_style == FontStyle::ITALIC {
// "font-style: italic"
// } else if font_style == FontStyle::UNDERLINE {
// "text-decoration: underline"
// } else {
// ""
// };
//
// 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>"#,
// 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,
// fs = fs
// ));
// }
// }
}
pub fn define() {

View File

@ -9,10 +9,10 @@ use jirs_data::*;
use crate::model::{ModalType, Model, Page};
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_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};
mod changes;
@ -85,7 +85,7 @@ pub enum Msg {
StrInputChanged(FieldId, String),
U32InputChanged(FieldId, u32),
FileInputChanged(FieldId, Vec<File>),
Rte(FieldId, RteMsg),
// Rte(FieldId, RteMsg),
// issues
AddIssue,
@ -279,9 +279,9 @@ pub fn render(host_url: String, ws_url: String) {
HOST_URL = host_url;
WS_URL = ws_url;
}
if !cfg!(debug_assertions) {
crate::hi::syntax_set::load();
}
// if !cfg!(debug_assertions) {
// crate::hi::syntax_set::load();
// }
elements::define();
let _app = seed::App::builder(update, view)

View File

@ -14,7 +14,7 @@ use crate::{
drag::DragState, styled_checkbox::StyledCheckboxState,
styled_date_time_input::StyledDateTimeInputState, styled_editor::Mode,
styled_image_input::StyledImageInputState, styled_input::StyledInputState,
styled_rte::StyledRteState, styled_select::StyledSelectState,
/*styled_rte::StyledRteState,*/ styled_select::StyledSelectState,
},
EditIssueModalSection, FieldId, Msg, ProjectFieldId,
};
@ -371,7 +371,7 @@ pub struct ProjectSettingsPage {
pub edit_column_id: Option<IssueStatusId>,
pub creating_issue_status: bool,
pub name: StyledInputState,
pub description_rte: StyledRteState,
// pub description_rte: StyledRteState,
}
impl ProjectSettingsPage {
@ -411,9 +411,9 @@ impl ProjectSettingsPage {
FieldId::ProjectSettings(ProjectFieldId::IssueStatusName),
"",
),
description_rte: StyledRteState::new(FieldId::ProjectSettings(
ProjectFieldId::Description,
)),
// description_rte: StyledRteState::new(FieldId::ProjectSettings(
// 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.time_tracking.update(&msg);
page.name.update(&msg);
page.description_rte.update(&msg, orders);
// page.description_rte.update(&msg, orders);
match msg {
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_icon::{Icon, StyledIcon};
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::{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_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 desc_rte = StyledField::build()
.input(
StyledRte::build(FieldId::ProjectSettings(ProjectFieldId::Description))
.state(&page.description_rte)
.build()
.into_node(),
)
.build()
.into_node();
// let desc_rte = StyledField::build()
// .input(
// StyledRte::build(FieldId::ProjectSettings(ProjectFieldId::Description))
// .state(&page.description_rte)
// .build()
// .into_node(),
// )
// .build()
// .into_node();
let category_field = category_field(page);
@ -89,7 +89,7 @@ pub fn view(model: &model::Model) -> Node<Msg> {
}))
.add_field(name_field)
.add_field(url_field)
.add_field(desc_rte)
// .add_field(desc_rte)
.add_field(description_field)
.add_field(category_field)
.add_field(time_tracking_field)

View File

@ -24,7 +24,7 @@ pub mod styled_image_input;
pub mod styled_input;
pub mod styled_link;
pub mod styled_modal;
pub mod styled_rte;
// pub mod styled_rte;
pub mod styled_select;
pub mod styled_select_child;
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>) {
match msg {
Msg::MessageInvitationApproved(token) => {
send_ws_msg(
WsMsg::InvitationAcceptRequest(*token),
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);
}
_ => (),
}
let m = match msg {
Msg::MessageInvitationApproved(token) => WsMsg::InvitationAcceptRequest(*token),
Msg::MessageInvitationDismiss(token) => WsMsg::InvitationRejectRequest(*token),
Msg::MessageSeen(id) => WsMsg::MessageMarkSeen(*id),
_ => return,
};
send_ws_msg(m, model.ws.as_ref(), orders);
}
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![
about_tooltip_popup(model),
messages_tooltip_popup(model),
@ -92,8 +88,7 @@ pub fn render(model: &Model) -> Vec<Node<Msg>> {
attrs![At::Href => "/"],
div![class!["styledLogo"], logo_svg]
],
navbar_left_item("Search issues", Icon::Search, None, None),
navbar_left_item("Create Issue", Icon::Plus, Some("/add-issue"), None),
issue_nav,
div![
class!["bottom"],
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>> {
let Message {
id,
receiver_id: _,
sender_id: _,
summary,
description,
message_type,
hyper_link,
created_at: _,
updated_at: _,
..
} = message;
let message_id = *id;
@ -203,10 +195,7 @@ fn message_ui(model: &Model, message: &Message) -> Option<Node<Msg>> {
let node = match message_type {
MessageType::ReceivedInvitation => {
let token: InvitationToken = match hyper_link.trim_start_matches('#').parse() {
Err(_) => return None,
Ok(n) => n,
};
let token: InvitationToken = hyper_link.trim_start_matches('#').parse().ok()?;
let accept = StyledButton::build()
.primary()
.text("Accept")

View File

@ -70,10 +70,10 @@ impl<'l> StyledButtonBuilder<'l> {
self.variant(Variant::Empty)
}
pub fn button_id(mut self, button_id: ButtonId) -> Self {
self.button_id = Some(button_id);
self
}
// pub fn button_id(mut self, button_id: ButtonId) -> Self {
// self.button_id = Some(button_id);
// self
// }
pub fn disabled(mut self, value: bool) -> Self {
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 {
if let Some(s) = state {
self.state(s)
} else {
self
}
}
// pub fn try_state<'state: 'l>(self, state: Option<&'state StyledSelectState>) -> Self {
// if let Some(s) = state {
// self.state(s)
// } else {
// self
// }
// }
pub fn state<'state: 'l>(self, state: &'state StyledSelectState) -> Self {
self.opened(state.opened)

View File

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

View File

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

View File

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

View File

@ -9,6 +9,114 @@ use crate::{
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)]
pub enum WsMsg {
Ping,
@ -124,4 +232,7 @@ pub enum WsMsg {
EpicUpdated(Epic),
EpicDelete(EpicId),
EpicDeleted(EpicId),
// errors
Error(WsError),
}

View File

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

View File

@ -1,16 +1,13 @@
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::{
db::{DbExecutor, DbPool, SyncQuery},
db::{tokens::FindAccessToken, DbExecutor, DbPool, DbPooledConn, SyncQuery},
db_pool,
errors::ServiceErrors,
};
#[derive(Serialize, Deserialize, Debug)]
pub struct AuthorizeUser {
pub access_token: uuid::Uuid,
}
@ -19,30 +16,26 @@ impl Message for AuthorizeUser {
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 {
type Result = Result<User, ServiceErrors>;
fn handle(&mut self, msg: AuthorizeUser, _: &mut Self::Context) -> Self::Result {
use crate::schema::tokens::dsl::{access_token, tokens};
use crate::schema::users::dsl::{id, users};
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)))
let conn = db_pool!(self);
msg.execute(conn)
}
}
@ -50,21 +43,10 @@ impl SyncQuery for AuthorizeUser {
type Result = std::result::Result<User, crate::errors::ServiceErrors>;
fn handle(&self, pool: &DbPool) -> Self::Result {
use crate::schema::tokens::dsl::{access_token, tokens};
use crate::schema::users::dsl::{id, users};
let conn = pool
.get()
.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)
let conn = pool.get().map_err(|e| {
error!("{:?}", e);
crate::errors::ServiceErrors::DatabaseConnectionLost
})?;
self.execute(&conn)
}
}

View File

@ -1,17 +1,32 @@
use actix::{Handler, Message};
use diesel::pg::Pg;
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 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 {
type Result = Result<Vec<Comment>, ServiceErrors>;
}
@ -20,28 +35,33 @@ impl Handler<LoadIssueComments> for DbExecutor {
type Result = Result<Vec<Comment>, ServiceErrors>;
fn handle(&mut self, msg: LoadIssueComments, _ctx: &mut Self::Context) -> Self::Result {
use crate::schema::comments::dsl::*;
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()))
let conn = db_pool!(self);
msg.execute(conn)
}
}
#[derive(Serialize, Deserialize)]
pub struct CreateComment {
pub user_id: i32,
pub issue_id: i32,
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 {
type Result = Result<Comment, ServiceErrors>;
}
@ -50,35 +70,35 @@ impl Handler<CreateComment> for DbExecutor {
type Result = Result<Comment, ServiceErrors>;
fn handle(&mut self, msg: CreateComment, _ctx: &mut Self::Context) -> Self::Result {
use crate::models::CommentForm;
use crate::schema::comments::dsl::*;
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()))
let conn = db_pool!(self);
msg.execute(conn)
}
}
#[derive(Serialize, Deserialize)]
pub struct UpdateComment {
pub comment_id: i32,
pub user_id: i32,
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 {
type Result = Result<Comment, ServiceErrors>;
}
@ -87,32 +107,32 @@ impl Handler<UpdateComment> for DbExecutor {
type Result = Result<Comment, ServiceErrors>;
fn handle(&mut self, msg: UpdateComment, _ctx: &mut Self::Context) -> Self::Result {
use crate::schema::comments::dsl::*;
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)
let conn = db_pool!(self);
msg.execute(conn)
}
}
#[derive(Serialize, Deserialize)]
pub struct DeleteComment {
pub comment_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 {
type Result = Result<(), ServiceErrors>;
}
@ -121,22 +141,8 @@ impl Handler<DeleteComment> for DbExecutor {
type Result = Result<(), ServiceErrors>;
fn handle(&mut self, msg: DeleteComment, _ctx: &mut Self::Context) -> Self::Result {
use crate::schema::comments::dsl::*;
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()))?;
let conn = db_pool!(self);
msg.execute(conn)?;
Ok(())
}
}

View File

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

View File

@ -1,22 +1,49 @@
use actix::{Handler, Message};
use diesel::connection::TransactionManager;
use diesel::pg::Pg;
use diesel::prelude::*;
use jirs_data::{
EmailString, Invitation, InvitationId, InvitationState, InvitationToken, ProjectId, Token,
User, UserId, UserRole, UsernameString,
msg::WsError, EmailString, Invitation, InvitationId, InvitationState, InvitationToken,
ProjectId, Token, User, UserId, UserRole, UsernameString,
};
use crate::db::DbPooledConn;
use crate::{
db::{
tokens::CreateBindToken,
users::{LookupUser, Register},
DbExecutor,
},
db_pool,
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 user_id: UserId,
}
@ -31,20 +58,18 @@ impl Handler<ListInvitation> for DbExecutor {
fn handle(&mut self, msg: ListInvitation, _ctx: &mut Self::Context) -> Self::Result {
use crate::schema::invitations::dsl::*;
let conn = &self
.pool
.get()
.map_err(|_| ServiceErrors::DatabaseConnectionLost)?;
let conn = db_pool!(self);
let query = invitations
q!(invitations
.filter(invited_by_id.eq(msg.user_id))
.filter(state.ne(InvitationState::Accepted))
.order_by(state.asc())
.then_order_by(updated_at.desc());
debug!("{}", diesel::debug_query::<Pg, _>(&query).to_string());
query
.then_order_by(updated_at.desc()))
.load(conn)
.map_err(|_| ServiceErrors::DatabaseConnectionLost)
.map_err(|e| {
error!("{:?}", e);
ServiceErrors::Error(WsError::FailedToLoadInvitations)
})
}
}
@ -56,6 +81,25 @@ pub struct CreateInvitation {
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 {
type Result = Result<Invitation, ServiceErrors>;
}
@ -64,25 +108,8 @@ impl Handler<CreateInvitation> for DbExecutor {
type Result = Result<Invitation, ServiceErrors>;
fn handle(&mut self, msg: CreateInvitation, _ctx: &mut Self::Context) -> Self::Result {
use crate::schema::invitations::dsl::*;
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)))
let conn = db_pool!(self);
msg.execute(conn)
}
}
@ -90,6 +117,18 @@ pub struct DeleteInvitation {
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 {
type Result = Result<(), ServiceErrors>;
}
@ -98,17 +137,8 @@ impl Handler<DeleteInvitation> for DbExecutor {
type Result = Result<(), ServiceErrors>;
fn handle(&mut self, msg: DeleteInvitation, _ctx: &mut Self::Context) -> Self::Result {
use crate::schema::invitations::dsl::*;
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)))?;
let conn = db_pool!(self);
msg.execute(conn)?;
Ok(())
}
}
@ -118,6 +148,24 @@ struct UpdateInvitationState {
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 {
type Result = Result<(), ServiceErrors>;
}
@ -126,23 +174,8 @@ impl Handler<UpdateInvitationState> for DbExecutor {
type Result = Result<(), ServiceErrors>;
fn handle(&mut self, msg: UpdateInvitationState, _ctx: &mut Self::Context) -> Self::Result {
use crate::schema::invitations::dsl::*;
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)))?;
let conn = db_pool!(self);
msg.execute(conn)?;
Ok(())
}
}
@ -158,14 +191,14 @@ impl Message for RevokeInvitation {
impl Handler<RevokeInvitation> for DbExecutor {
type Result = Result<(), ServiceErrors>;
fn handle(&mut self, msg: RevokeInvitation, ctx: &mut Self::Context) -> Self::Result {
self.handle(
fn handle(&mut self, msg: RevokeInvitation, _ctx: &mut Self::Context) -> Self::Result {
let conn = db_pool!(self);
UpdateInvitationState {
id: msg.id,
state: InvitationState::Revoked,
},
ctx,
)
}
.execute(conn)?;
Ok(())
}
}
@ -173,6 +206,85 @@ pub struct AcceptInvitation {
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 {
type Result = Result<Token, ServiceErrors>;
}
@ -180,107 +292,9 @@ impl Message for AcceptInvitation {
impl Handler<AcceptInvitation> for DbExecutor {
type Result = Result<Token, ServiceErrors>;
fn handle(&mut self, msg: AcceptInvitation, ctx: &mut Self::Context) -> Self::Result {
use crate::schema::invitations::dsl::*;
fn handle(&mut self, msg: AcceptInvitation, _ctx: &mut Self::Context) -> Self::Result {
let conn = db_pool!(self);
let conn = &self
.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)
msg.execute(conn)
}
}

View File

@ -1,17 +1,34 @@
use actix::{Handler, Message};
use diesel::pg::Pg;
use diesel::prelude::*;
use serde::{Deserialize, Serialize};
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 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 {
type Result = Result<Vec<IssueAssignee>, ServiceErrors>;
}
@ -20,18 +37,8 @@ impl Handler<LoadAssignees> for DbExecutor {
type Result = Result<Vec<IssueAssignee>, ServiceErrors>;
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
.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()))
msg.execute(conn)
}
}

View File

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

View File

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

View File

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

View File

@ -5,6 +5,8 @@ use diesel::pg::PgConnection;
use diesel::r2d2::{self, ConnectionManager};
use serde::{Deserialize, Serialize};
use crate::errors::ServiceErrors;
pub mod authorize_user;
pub mod comments;
pub mod epics;
@ -105,3 +107,68 @@ impl Configuration {
"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 diesel::pg::Pg;
use diesel::prelude::*;
use serde::{Deserialize, Serialize};
use jirs_data::{NameString, Project, ProjectCategory, ProjectId, TimeTracking, UserId};
use crate::db::DbExecutor;
use crate::errors::ServiceErrors;
use crate::schema::projects::all_columns;
use crate::db::DbPooledConn;
use crate::{db::DbExecutor, db_pool, errors::ServiceErrors, q, schema::projects::all_columns};
#[derive(Serialize, Deserialize)]
pub struct LoadCurrentProject {
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 {
type Result = Result<Project, ServiceErrors>;
}
@ -22,26 +33,12 @@ impl Handler<LoadCurrentProject> for DbExecutor {
type Result = Result<Project, ServiceErrors>;
fn handle(&mut self, msg: LoadCurrentProject, _ctx: &mut Self::Context) -> Self::Result {
use crate::schema::projects::dsl::projects;
let conn = &self
.pool
.get()
.map_err(|_| ServiceErrors::DatabaseConnectionLost)?;
let conn = db_pool!(self);
let query = projects.find(msg.project_id);
debug!(
"{}",
diesel::debug_query::<diesel::pg::Pg, _>(&query).to_string()
);
query
.first::<Project>(conn)
.map_err(|_| ServiceErrors::RecordNotFound("Project".to_string()))
msg.execute(conn)
}
}
#[derive(Serialize, Deserialize)]
pub struct CreateProject {
pub name: NameString,
pub url: Option<String>,
@ -50,6 +47,38 @@ pub struct CreateProject {
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 {
type Result = Result<Project, ServiceErrors>;
}
@ -58,29 +87,12 @@ impl Handler<CreateProject> for DbExecutor {
type Result = Result<Project, ServiceErrors>;
fn handle(&mut self, msg: CreateProject, _ctx: &mut Self::Context) -> Self::Result {
use crate::schema::projects::dsl::*;
let conn = &self
.pool
.get()
.map_err(|_| ServiceErrors::DatabaseConnectionLost)?;
let conn = db_pool!(self);
let query = diesel::insert_into(projects)
.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)))
msg.execute(conn)
}
}
#[derive(Serialize, Deserialize)]
pub struct UpdateProject {
pub project_id: ProjectId,
pub name: Option<NameString>,
@ -90,6 +102,27 @@ pub struct UpdateProject {
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 {
type Result = Result<Project, ServiceErrors>;
}
@ -98,29 +131,9 @@ impl Handler<UpdateProject> for DbExecutor {
type Result = Result<Project, ServiceErrors>;
fn handle(&mut self, msg: UpdateProject, _ctx: &mut Self::Context) -> Self::Result {
use crate::schema::projects::dsl::*;
let conn = &self
.pool
.get()
.map_err(|_| ServiceErrors::DatabaseConnectionLost)?;
let conn = db_pool!(self);
let update_query = diesel::update(projects.find(msg.project_id)).set((
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()))
msg.execute(conn)
}
}
@ -128,6 +141,24 @@ pub struct LoadProjects {
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 {
type Result = Result<Vec<Project>, ServiceErrors>;
}
@ -136,22 +167,8 @@ impl Handler<LoadProjects> for DbExecutor {
type Result = Result<Vec<Project>, ServiceErrors>;
fn handle(&mut self, msg: LoadProjects, _ctx: &mut Self::Context) -> Self::Result {
use crate::schema::projects::dsl::*;
use crate::schema::user_projects::dsl::{project_id, user_id, user_projects};
let conn = db_pool!(self);
let conn = &self
.pool
.get()
.map_err(|_| ServiceErrors::DatabaseConnectionLost)?;
let query = projects
.inner_join(user_projects.on(project_id.eq(id)))
.filter(user_id.eq(msg.user_id))
.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()))
msg.execute(conn)
}
}

View File

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

View File

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

View File

@ -1,21 +1,34 @@
use actix::{Handler, Message};
use diesel::connection::TransactionManager;
use diesel::pg::Pg;
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::{DbExecutor, DbPooledConn};
use crate::errors::ServiceErrors;
use crate::schema::users::all_columns;
use crate::db::user_projects::CreateUserProject;
use crate::{
db::{projects::CreateProject, DbExecutor, DbPooledConn},
db_pool,
errors::ServiceErrors,
q,
schema::users::all_columns,
};
#[derive(Debug)]
pub struct FindUser {
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 {
type Result = Result<User, ServiceErrors>;
}
@ -24,27 +37,32 @@ impl Handler<FindUser> for DbExecutor {
type Result = Result<User, ServiceErrors>;
fn handle(&mut self, msg: FindUser, _ctx: &mut Self::Context) -> Self::Result {
use crate::schema::users::dsl::*;
let conn = &self
.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)))
let conn = db_pool!(self);
msg.execute(conn)
}
}
#[derive(Debug)]
pub struct LookupUser {
pub name: 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 {
type Result = Result<User, ServiceErrors>;
}
@ -53,29 +71,33 @@ impl Handler<LookupUser> for DbExecutor {
type Result = Result<User, ServiceErrors>;
fn handle(&mut self, msg: LookupUser, _ctx: &mut Self::Context) -> Self::Result {
use crate::schema::users::dsl::*;
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)))
let conn = db_pool!(self);
msg.execute(conn)
}
}
#[derive(Debug)]
pub struct LoadProjectUsers {
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 {
type Result = Result<Vec<User>, ServiceErrors>;
}
@ -84,31 +106,33 @@ impl Handler<LoadProjectUsers> for DbExecutor {
type Result = Result<Vec<User>, ServiceErrors>;
fn handle(&mut self, msg: LoadProjectUsers, _ctx: &mut Self::Context) -> Self::Result {
use crate::schema::user_projects::dsl::{project_id, user_id, user_projects};
use crate::schema::users::dsl::*;
let conn = &self
.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()))
let conn = db_pool!(self);
msg.execute(conn)
}
}
#[derive(Debug)]
pub struct LoadIssueAssignees {
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 {
type Result = Result<Vec<User>, ServiceErrors>;
}
@ -117,31 +141,110 @@ impl Handler<LoadIssueAssignees> for DbExecutor {
type Result = Result<Vec<User>, ServiceErrors>;
fn handle(&mut self, msg: LoadIssueAssignees, _ctx: &mut Self::Context) -> Self::Result {
use crate::schema::issue_assignees::dsl::{issue_assignees, issue_id, user_id};
use crate::schema::users::dsl::*;
let conn = &self
.pool
.get()
.map_err(|_| ServiceErrors::DatabaseConnectionLost)?;
let users_query = users
.distinct_on(id)
.inner_join(issue_assignees.on(user_id.eq(id)))
.filter(issue_id.eq(msg.issue_id))
.select(users::all_columns());
debug!("{}", diesel::debug_query::<Pg, _>(&users_query));
users_query
.load(conn)
.map_err(|_| ServiceErrors::RecordNotFound("issue users".to_string()))
let conn = db_pool!(self);
msg.execute(conn)
}
}
pub struct CreateUser {
pub name: UsernameString,
pub email: EmailString,
}
impl CreateUser {
pub fn execute(self, conn: &DbPooledConn) -> Result<User, ServiceErrors> {
use crate::schema::users::dsl::*;
q!(diesel::insert_into(users)
.values((name.eq(self.name.as_str()), email.eq(self.email.as_str()))))
.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 name: String,
pub email: String,
pub name: UsernameString,
pub email: EmailString,
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 {
@ -151,74 +254,12 @@ impl Message for Register {
impl Handler<Register> for DbExecutor {
type Result = Result<(), ServiceErrors>;
fn handle(&mut self, msg: Register, ctx: &mut Self::Context) -> Self::Result {
use crate::schema::users::dsl::*;
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(())
fn handle(&mut self, msg: Register, _ctx: &mut Self::Context) -> Self::Result {
let conn = db_pool!(self);
msg.execute(conn)
}
}
#[derive(Serialize, Deserialize, Debug)]
pub struct LoadInvitedUsers {
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::users::dsl::{email as u_email, users};
let conn = &self
.pool
.get()
.map_err(|_| ServiceErrors::DatabaseConnectionLost)?;
let conn = db_pool!(self);
let query = users
q!(users
.inner_join(invitations.on(i_email.eq(u_email)))
.filter(invited_by_id.eq(msg.user_id))
.select(users::all_columns());
debug!("{}", diesel::debug_query::<Pg, _>(&query));
query
.select(users::all_columns()))
.load(conn)
.map_err(|e| ServiceErrors::DatabaseQueryFailed(format!("{}", e)))
.map_err(|e| {
error!("{:?}", e);
ServiceErrors::Error(WsError::FailedToLoadInvitedUsers)
})
}
}
fn count_matching_users(name: &str, email: &str, conn: &DbPooledConn) -> i64 {
use crate::schema::users::dsl;
let query = dsl::users
q!(dsl::users
.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.eq(email).and(dsl::name.eq(name)))
.count();
info!("{}", diesel::debug_query::<diesel::pg::Pg, _>(&query));
query.get_result::<i64>(conn).unwrap_or(1)
.count())
.get_result::<i64>(conn)
.unwrap_or(1)
}
#[derive(Serialize, Deserialize, Debug)]
pub struct UpdateAvatarUrl {
pub user_id: UserId,
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 {
use crate::schema::users::dsl::{avatar_url, id, users};
let conn = &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 conn = db_pool!(self);
let user_query = users.find(msg.user_id);
debug!("{}", diesel::debug_query::<Pg, _>(&user_query));
user_query
.first(conn)
.map_err(|e| ServiceErrors::DatabaseQueryFailed(format!("{}", e)))
q!(diesel::update(users)
.set(avatar_url.eq(msg.avatar_url))
.filter(id.eq(msg.user_id)))
.execute(conn)
.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 user_id: UserId,
pub name: String,
@ -315,21 +350,15 @@ impl Handler<ProfileUpdate> for DbExecutor {
fn handle(&mut self, msg: ProfileUpdate, _ctx: &mut Self::Context) -> Self::Result {
use crate::schema::users::dsl::{email, id, name, users};
let conn = &self
.pool
.get()
.map_err(|_| ServiceErrors::DatabaseConnectionLost)?;
let update_query = diesel::update(users)
let conn = db_pool!(self);
q!(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
.filter(id.eq(msg.user_id)))
.execute(conn)
.map_err(|e| ServiceErrors::DatabaseQueryFailed(format!("{}", e)))?;
let user_query = users.find(msg.user_id);
debug!("{}", diesel::debug_query::<Pg, _>(&user_query));
user_query
q!(users.find(msg.user_id))
.first(conn)
.map_err(|e| ServiceErrors::DatabaseQueryFailed(format!("{}", e)))
}

View File

@ -1,6 +1,6 @@
use actix_web::HttpResponse;
use jirs_data::ErrorResponse;
use jirs_data::{msg::WsError, ErrorResponse};
const TOKEN_NOT_FOUND: &str = "Token not found";
const DATABASE_CONNECTION_FAILED: &str = "Database connection failed";
@ -12,6 +12,7 @@ pub enum ServiceErrors {
DatabaseQueryFailed(String),
RecordNotFound(String),
RegisterCollision,
Error(WsError),
}
impl ServiceErrors {
@ -44,6 +45,9 @@ impl Into<HttpResponse> for ServiceErrors {
ServiceErrors::RegisterCollision => HttpResponse::Unauthorized().json(ErrorResponse {
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;
use actix::Actor;
use actix_cors::Cors;
// use actix_cors::Cors;
#[cfg(feature = "local-storage")]
use actix_files as fs;
use actix_web::{App, HttpServer};
use crate::ws::WsServer;
// use actix_web::http::Method;
pub mod db;
pub mod errors;
pub mod mail;
@ -53,7 +55,6 @@ async fn main() -> Result<(), String> {
HttpServer::new(move || {
let app = App::new()
.wrap(actix_web::middleware::Logger::default())
.wrap(Cors::default())
.data(ws_server.clone())
.data(db_addr.clone())
.data(mail_addr.clone())

View File

@ -9,14 +9,6 @@ use jirs_data::{
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)]
pub struct Issue {
pub id: i32,

View File

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

View File

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