diff --git a/.gitignore b/.gitignore index 6954e4c4..23b6cc98 100644 --- a/.gitignore +++ b/.gitignore @@ -1,10 +1,16 @@ /target + mail.toml mail.test.toml web.toml web.test.toml db.toml db.test.toml +fs.toml +fs.test.toml +highlight.toml +highlight.test.toml + pkg jirs-client/pkg jirs-client/tmp @@ -15,3 +21,5 @@ jirs-cli/target jirs-bat/bat highlight/jirs-highlight/build +uploads +config diff --git a/Cargo.lock b/Cargo.lock index 69748fbe..8cf1451f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -10,7 +10,7 @@ dependencies = [ "actix-rt", "actix_derive", "bitflags", - "bytes", + "bytes 0.5.6", "crossbeam-channel", "derive_more", "futures 0.3.8", @@ -34,7 +34,7 @@ dependencies = [ "actix-rt", "actix_derive", "bitflags", - "bytes", + "bytes 0.5.6", "crossbeam-channel", "derive_more", "futures-channel", @@ -57,7 +57,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "09e55f0a5c2ca15795035d90c46bd0e73a5123b72f68f12596d6ba5282051380" dependencies = [ "bitflags", - "bytes", + "bytes 0.5.6", "futures-core", "futures-sink", "log", @@ -72,7 +72,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "78d1833b3838dbe990df0f1f87baf640cf6146e898166afe401839d1b001e570" dependencies = [ "bitflags", - "bytes", + "bytes 0.5.6", "futures-core", "futures-sink", "log", @@ -135,14 +135,14 @@ dependencies = [ [[package]] name = "actix-files" -version = "0.4.0" +version = "0.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5fc0a9181e93c91dc7eb401a0debaed5c8294e12019c307c72fd7a1731b672fc" +checksum = "d031468a7859f71674e5531bd05137e0ea5de05ec9a917314330b88c582e2e0a" dependencies = [ "actix-service", "actix-web", "bitflags", - "bytes", + "bytes 0.5.6", "derive_more", "futures-core", "futures-util", @@ -167,7 +167,7 @@ dependencies = [ "actix-utils 1.0.6", "base64 0.11.0", "bitflags", - "bytes", + "bytes 0.5.6", "chrono", "copyless", "derive_more", @@ -191,7 +191,7 @@ dependencies = [ "regex", "serde", "serde_json", - "serde_urlencoded", + "serde_urlencoded 0.6.1", "sha1", "slab", "time 0.1.44", @@ -199,9 +199,9 @@ dependencies = [ [[package]] name = "actix-http" -version = "2.1.0" +version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "404df68c297f73b8d36c9c9056404913d25905a8f80127b0e5fe147c9c4b9f02" +checksum = "452299e87817ae5673910e53c243484ca38be3828db819b6011736fc6982e874" dependencies = [ "actix-codec 0.3.0", "actix-connect 2.0.0", @@ -212,7 +212,7 @@ dependencies = [ "base64 0.13.0", "bitflags", "brotli2", - "bytes", + "bytes 0.5.6", "cookie 0.14.3", "copyless", "derive_more", @@ -238,7 +238,7 @@ dependencies = [ "regex", "serde", "serde_json", - "serde_urlencoded", + "serde_urlencoded 0.7.0", "sha-1 0.9.2", "slab", "time 0.2.23", @@ -246,9 +246,9 @@ dependencies = [ [[package]] name = "actix-macros" -version = "0.1.2" +version = "0.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a60f9ba7c4e6df97f3aacb14bb5c0cd7d98a49dcbaed0d7f292912ad9a6a3ed2" +checksum = "b4ca8ce00b267af8ccebbd647de0d61e0674b6e61185cc7a592ff88772bed655" dependencies = [ "quote", "syn", @@ -263,7 +263,7 @@ dependencies = [ "actix-service", "actix-utils 2.0.0", "actix-web", - "bytes", + "bytes 0.5.6", "derive_more", "futures-util", "httparse", @@ -381,7 +381,7 @@ dependencies = [ "actix-rt", "actix-service", "bitflags", - "bytes", + "bytes 0.5.6", "either", "futures 0.3.8", "log", @@ -399,7 +399,7 @@ dependencies = [ "actix-rt", "actix-service", "bitflags", - "bytes", + "bytes 0.5.6", "either", "futures-channel", "futures-sink", @@ -411,12 +411,12 @@ dependencies = [ [[package]] name = "actix-web" -version = "3.2.0" +version = "3.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "88344b7a5ef27e5e09e73565379f69273dd3e2d29e82afc381b84d170d0a5631" +checksum = "e641d4a172e7faa0862241a20ff4f1f5ab0ab7c279f00c2d4587b77483477b86" dependencies = [ "actix-codec 0.3.0", - "actix-http 2.1.0", + "actix-http 2.2.0", "actix-macros", "actix-router", "actix-rt", @@ -428,7 +428,7 @@ dependencies = [ "actix-utils 2.0.0", "actix-web-codegen", "awc", - "bytes", + "bytes 0.5.6", "derive_more", "encoding_rs", "futures-channel", @@ -441,7 +441,7 @@ dependencies = [ "regex", "serde", "serde_json", - "serde_urlencoded", + "serde_urlencoded 0.7.0", "socket2", "time 0.2.23", "tinyvec", @@ -456,9 +456,9 @@ checksum = "7f6edf3c2693e2a8c422800c87ee89a6a4eac7dd01109bc172a1093ce1f4f001" dependencies = [ "actix 0.10.0", "actix-codec 0.3.0", - "actix-http 2.1.0", + "actix-http 2.2.0", "actix-web", - "bytes", + "bytes 0.5.6", "futures-channel", "futures-core", "pin-project 0.4.27", @@ -573,16 +573,16 @@ checksum = "cdb031dd78e28731d87d56cc8ffef4a8f36ca26c38fe2de700543e627f8a464a" [[package]] name = "awc" -version = "2.0.1" +version = "2.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "425980a1e58e5030a3e4b065a3d577c8f0e16142ea9d81f30614eae810c98577" +checksum = "b381e490e7b0cfc37ebc54079b0413d8093ef43d14a4e4747083f7fa47a9e691" dependencies = [ "actix-codec 0.3.0", - "actix-http 2.1.0", + "actix-http 2.2.0", "actix-rt", "actix-service", "base64 0.13.0", - "bytes", + "bytes 0.5.6", "cfg-if 1.0.0", "derive_more", "futures-core", @@ -592,7 +592,7 @@ dependencies = [ "rand 0.7.3", "serde", "serde_json", - "serde_urlencoded", + "serde_urlencoded 0.7.0", ] [[package]] @@ -742,11 +742,11 @@ dependencies = [ [[package]] name = "buf-min" -version = "0.1.1" +version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b6ae7069aad07c7cdefe6a22a671f00650728bd2331a4cc62e1e5d0becdf9ca4" +checksum = "881e704e61d0fb41d7c6c9ae2bd790eb8c13dc974ae102fb98c788b4fdea4349" dependencies = [ - "bytes", + "bytes 0.6.0", ] [[package]] @@ -779,13 +779,19 @@ version = "0.5.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0e4cec68f03f32e44924783795810fa50a7035d8c8ebe78580ad7e6c703fba38" +[[package]] +name = "bytes" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e0dcbc35f504eb6fc275a6d20e4ebcda18cf50d40ba6fabff8c711fa16cb3b16" + [[package]] name = "bytestring" version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fc7c05fa5172da78a62d9949d662d2ac89d4cc7355d7b49adee5163f1fb3f363" dependencies = [ - "bytes", + "bytes 0.5.6", ] [[package]] @@ -796,9 +802,9 @@ checksum = "df8670b8c7b9dae1793364eafadf7239c40d669904660c5960d74cfd80b46a53" [[package]] name = "cc" -version = "1.0.65" +version = "1.0.66" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "95752358c8f7552394baf48cd82695b345628ad3f170d607de3ca03b8dacca15" +checksum = "4c0496836a84f8d0495758516b8621a622beb77c0fed418570e50764093ced48" [[package]] name = "cfg-if" @@ -852,15 +858,6 @@ dependencies = [ "bitflags", ] -[[package]] -name = "cloudabi" -version = "0.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4344512281c643ae7638bbabc3af17a11307803ec8f0fcad9fae512a8bf36467" -dependencies = [ - "bitflags", -] - [[package]] name = "comrak" version = "0.9.0" @@ -892,9 +889,9 @@ dependencies = [ [[package]] name = "const_fn" -version = "0.4.3" +version = "0.4.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c478836e029dcef17fb47c89023448c64f781a046e0300e257ad8225ae59afab" +checksum = "cd51eab21ab4fd6a3bf889e2d0958c0a6e3a61ad04260325e919e652a2a62826" [[package]] name = "constant_time_eq" @@ -966,7 +963,7 @@ version = "0.4.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b153fe7cbef478c567df0f972e02e6d736db11affe43dfc9c56a9374d1adfb87" dependencies = [ - "crossbeam-utils", + "crossbeam-utils 0.7.2", "maybe-uninit", ] @@ -982,15 +979,65 @@ dependencies = [ ] [[package]] -name = "crypto-mac" -version = "0.7.0" +name = "crossbeam-utils" +version = "0.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4434400df11d95d556bac068ddfedd482915eb18fe8bea89bc80b6e4b1c179e5" +checksum = "02d96d1e189ef58269ebe5b97953da3274d83a93af647c2ddd6f9dab28cedb8d" dependencies = [ - "generic-array 0.12.3", + "autocfg 1.0.1", + "cfg-if 1.0.0", + "lazy_static", +] + +[[package]] +name = "crypto-mac" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b584a330336237c1eecd3e94266efb216c56ed91225d634cb2991c5f3fd1aeab" +dependencies = [ + "generic-array 0.14.4", "subtle", ] +[[package]] +name = "database-actor" +version = "0.1.0" +dependencies = [ + "actix 0.10.0", + "actix-web", + "bigdecimal", + "bincode", + "bitflags", + "byteorder", + "chrono", + "diesel", + "dotenv", + "env_logger", + "futures 0.3.8", + "ipnetwork", + "jirs-config", + "jirs-data", + "libc", + "log", + "num-bigint", + "num-integer", + "num-traits", + "openssl-sys", + "percent-encoding", + "pq-sys", + "pretty_env_logger", + "r2d2", + "serde", + "time 0.1.44", + "toml", + "url", + "uuid 0.8.1", +] + +[[package]] +name = "database-actor-derive" +version = "0.1.0" + [[package]] name = "dbg" version = "1.0.4" @@ -1271,6 +1318,21 @@ dependencies = [ "ascii_utils", ] +[[package]] +name = "filesystem-actor" +version = "0.1.0" +dependencies = [ + "actix 0.10.0", + "actix-files", + "bytes 0.5.6", + "env_logger", + "futures 0.3.8", + "jirs-config", + "log", + "pretty_env_logger", + "tokio", +] + [[package]] name = "filetime" version = "0.2.13" @@ -1591,7 +1653,7 @@ version = "0.2.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5e4728fd124914ad25e99e3d15a9361a879f6620f63cb56bbb08f95abb97a535" dependencies = [ - "bytes", + "bytes 0.5.6", "fnv", "futures-core", "futures-sink", @@ -1613,9 +1675,9 @@ checksum = "d7afe4a420e3fe79967a00898cc1f4db7c8a49a9333a29f8a4bd76a253d5cd04" [[package]] name = "heck" -version = "0.3.1" +version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "20564e78d53d2bb135c343b3f47714a56af2061f1c928fdb541dc7b9fdd94205" +checksum = "87cbf45460356b7deeb5e3415b5563308c0a9b057c85e12b06ad551f98d0a6ac" dependencies = [ "unicode-segmentation", ] @@ -1635,14 +1697,32 @@ version = "0.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "644f9158b2f133fd50f5fb3242878846d9eb792e445c893805ff0e3824006e35" +[[package]] +name = "highlight-actor" +version = "0.1.0" +dependencies = [ + "actix 0.10.0", + "bincode", + "env_logger", + "flate2", + "jirs-config", + "jirs-data", + "lazy_static", + "log", + "pretty_env_logger", + "serde", + "syntect", + "toml", +] + [[package]] name = "hmac" -version = "0.7.1" +version = "0.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5dcb5e64cda4c23119ab41ba960d1e170a774c8e4b9d9e6a9bc18aabf5e59695" +checksum = "126888268dcc288495a26bf004b38c5fdbb31682f992c84ceb046a1f0fe38840" dependencies = [ "crypto-mac", - "digest 0.8.1", + "digest 0.9.0", ] [[package]] @@ -1668,11 +1748,11 @@ dependencies = [ [[package]] name = "http" -version = "0.2.1" +version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "28d569972648b2c512421b5f2a405ad6ac9666547189d0c5477a3f200f3e02f9" +checksum = "84129d298a6d57d246960ff8eb831ca4af3f96d29e2e28848dae275408658e26" dependencies = [ - "bytes", + "bytes 0.5.6", "fnv", "itoa", ] @@ -1683,7 +1763,7 @@ version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "13d5ff830006f7646652e057693569bfe0d51760c0085a071769d142a205111b" dependencies = [ - "bytes", + "bytes 0.5.6", "http", ] @@ -1714,7 +1794,7 @@ version = "0.13.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f6ad767baac13b44d4529fcf58ba2cd0995e36e7b435bc5b039de6f47e880dbf" dependencies = [ - "bytes", + "bytes 0.5.6", "futures-channel", "futures-core", "futures-util", @@ -1738,7 +1818,7 @@ version = "0.4.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d979acc56dcb5b8dddba3917601745e877576475aa046df3226eabdecef78eed" dependencies = [ - "bytes", + "bytes 0.5.6", "hyper", "native-tls", "tokio", @@ -1758,9 +1838,9 @@ dependencies = [ [[package]] name = "indexmap" -version = "1.6.0" +version = "1.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "55e2e4c765aa53a0424761bf9f41aa7a6ac1efa87238f59560640e27fca028f2" +checksum = "4fb1fa934250de4de8aef298d81c729a7d33d8c239daa3a7575e6b92bfc7313b" dependencies = [ "autocfg 1.0.1", "hashbrown", @@ -1851,6 +1931,17 @@ dependencies = [ "tui", ] +[[package]] +name = "jirs-config" +version = "0.1.0" +dependencies = [ + "rusoto_core", + "rusoto_s3", + "rusoto_signature", + "serde", + "toml", +] + [[package]] name = "jirs-css" version = "0.1.0" @@ -1864,6 +1955,7 @@ dependencies = [ name = "jirs-data" version = "0.1.0" dependencies = [ + "actix 0.10.0", "chrono", "diesel", "serde", @@ -1877,8 +1969,6 @@ version = "0.1.0" dependencies = [ "actix 0.10.0", "actix-cors", - "actix-files", - "actix-multipart", "actix-rt", "actix-service", "actix-web", @@ -1889,36 +1979,33 @@ dependencies = [ "bitflags", "byteorder", "chrono", - "diesel", + "database-actor", "dotenv", "env_logger", - "flate2", + "filesystem-actor", "futures 0.3.8", + "highlight-actor", "ipnetwork", + "jirs-config", "jirs-data", - "lazy_static", - "lettre", - "lettre_email", "libc", "log", + "mail-actor", "num-bigint", "num-integer", "num-traits", "openssl-sys", "percent-encoding", - "pq-sys", "pretty_env_logger", - "r2d2", - "rusoto_core", - "rusoto_s3", "serde", "serde_json", - "syntect", "time 0.1.44", "tokio", "toml", "url", "uuid 0.8.1", + "web-actor", + "websocket-actor", ] [[package]] @@ -1943,9 +2030,9 @@ dependencies = [ [[package]] name = "js-sys" -version = "0.3.45" +version = "0.3.46" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ca059e81d9486668f12d455a4ea6daa600bd408134cd17e3d3fb5a32d1f016f8" +checksum = "cf3d7383929f7c9c7c2d0fa596f325832df98c3704f2c60553080f7127a58175" dependencies = [ "wasm-bindgen", ] @@ -2012,9 +2099,9 @@ dependencies = [ [[package]] name = "libc" -version = "0.2.80" +version = "0.2.81" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4d58d1b70b004888f764dfbf6a26a3b0342a1632d33968e4a179d8011c760614" +checksum = "1482821306169ec4d07f6aca392a4681f66c75c9918aa49641a2595db64053cb" [[package]] name = "line-wrap" @@ -2067,6 +2154,26 @@ dependencies = [ "linked-hash-map", ] +[[package]] +name = "mail-actor" +version = "0.1.0" +dependencies = [ + "actix 0.10.0", + "dotenv", + "env_logger", + "futures 0.3.8", + "jirs-config", + "lettre", + "lettre_email", + "libc", + "log", + "openssl-sys", + "pretty_env_logger", + "serde", + "toml", + "uuid 0.8.1", +] + [[package]] name = "maplit" version = "1.0.2" @@ -2137,9 +2244,9 @@ dependencies = [ [[package]] name = "mio" -version = "0.6.22" +version = "0.6.23" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fce347092656428bc8eaf6201042cb551b8d67855af7374542a92a0fbfcac430" +checksum = "4afd66f5b91bf2a3bc13fad0e21caedac168ca4c707504e75585648ae80e4cc4" dependencies = [ "cfg-if 0.1.10", "fuchsia-zircon", @@ -2148,7 +2255,7 @@ dependencies = [ "kernel32-sys", "libc", "log", - "miow 0.2.1", + "miow 0.2.2", "net2", "slab", "winapi 0.2.8", @@ -2191,9 +2298,9 @@ dependencies = [ [[package]] name = "miow" -version = "0.2.1" +version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8c1f2f3b1cf331de6896aabf6e9d55dca90356cc9960cca7eaaf408a355ae919" +checksum = "ebd808424166322d4a38da87083bfddd3ac4c131334ed55856112eb06d46944d" dependencies = [ "kernel32-sys", "net2", @@ -2231,9 +2338,9 @@ dependencies = [ [[package]] name = "net2" -version = "0.2.35" +version = "0.2.37" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3ebc3ec692ed7c9a255596c67808dee269f64655d8baf7b4f0638e51ba1d6853" +checksum = "391630d12b68002ae1e25e8f974306474966550ad82dac6886fb8910c19568ae" dependencies = [ "cfg-if 0.1.10", "libc", @@ -2362,12 +2469,12 @@ checksum = "624a8340c38c1b80fd549087862da4ba43e08858af025b236e509b6649fc13d5" [[package]] name = "openssl" -version = "0.10.30" +version = "0.10.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8d575eff3665419f9b83678ff2815858ad9d11567e082f5ac1814baba4e2bcb4" +checksum = "8d008f51b1acffa0d3450a68606e6a51c123012edaacb0f4e1426bd978869187" dependencies = [ "bitflags", - "cfg-if 0.1.10", + "cfg-if 1.0.0", "foreign-types", "lazy_static", "libc", @@ -2382,18 +2489,18 @@ checksum = "77af24da69f9d9341038eba93a073b1fdaaa1b788221b00a69bce9e762cb32de" [[package]] name = "openssl-src" -version = "111.12.0+1.1.1h" +version = "111.13.0+1.1.1i" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "858a4132194f8570a7ee9eb8629e85b23cbc4565f2d4a162e87556e5956abf61" +checksum = "045e4dc48af57aad93d665885789b43222ae26f4886494da12d1ed58d309dcb6" dependencies = [ "cc", ] [[package]] name = "openssl-sys" -version = "0.9.58" +version = "0.9.59" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a842db4709b604f0fe5d1170ae3565899be2ad3d9cbc72dedc789ac0511f78de" +checksum = "de52d8eabd217311538a39bba130d7dea1f1e118010fee7a033d966845e7d5fe" dependencies = [ "autocfg 1.0.1", "cc", @@ -2421,7 +2528,7 @@ checksum = "6d7744ac029df22dca6284efe4e898991d28e3085c706c972bcd7da4a27a15eb" dependencies = [ "instant", "lock_api 0.4.2", - "parking_lot_core 0.8.0", + "parking_lot_core 0.8.2", ] [[package]] @@ -2431,7 +2538,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d58c7c768d4ba344e3e8d72518ac13e259d7c7ade24167003b8488e10b6740a3" dependencies = [ "cfg-if 0.1.10", - "cloudabi 0.0.3", + "cloudabi", "libc", "redox_syscall", "smallvec", @@ -2440,12 +2547,11 @@ dependencies = [ [[package]] name = "parking_lot_core" -version = "0.8.0" +version = "0.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c361aa727dd08437f2f1447be8b59a33b0edd15e0fcee698f935613d9efbca9b" +checksum = "9ccb628cad4f84851442432c60ad8e1f607e29752d0bf072cbd0baf28aa34272" dependencies = [ - "cfg-if 0.1.10", - "cloudabi 0.1.0", + "cfg-if 1.0.0", "instant", "libc", "redox_syscall", @@ -2548,6 +2654,12 @@ version = "0.1.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c917123afa01924fc84bb20c4c03f004d9c38e5127e3c039bbf7f4b9c76a2f6b" +[[package]] +name = "pin-project-lite" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6b063f57ec186e6140e2b8b6921e5f1bd89c7356dda5b33acc5401203ca6131c" + [[package]] name = "pin-utils" version = "0.1.0" @@ -2640,9 +2752,9 @@ checksum = "a1d01941d82fa2ab50be1e79e6714289dd7cde78eba4c074bc5a4374f650dfe0" [[package]] name = "quote" -version = "1.0.7" +version = "1.0.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "aa563d17ecb180e500da1cfd2b028310ac758de548efdd203e18f283af693f37" +checksum = "991431c3519a3f36861882da93630ce66b52918dcf1b8e2fd66b397fc96f28df" dependencies = [ "proc-macro2", ] @@ -2792,7 +2904,7 @@ version = "0.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7b75f676a1e053fc562eafbb47838d67c84801e38fc1ba459e8f180deabd5071" dependencies = [ - "cloudabi 0.0.3", + "cloudabi", "fuchsia-cprng", "libc", "rand_core 0.4.2", @@ -2912,15 +3024,15 @@ dependencies = [ [[package]] name = "rusoto_core" -version = "0.43.0" +version = "0.45.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3a8d624cb48fcaca612329e4dd544380aa329ef338e83d3a90f5b7897e631971" +checksum = "e977941ee0658df96fca7291ecc6fc9a754600b21ad84b959eb1dbbc9d5abcc7" dependencies = [ "async-trait", "base64 0.12.3", - "bytes", + "bytes 0.5.6", + "crc32fast", "futures 0.3.8", - "hmac", "http", "hyper", "hyper-tls", @@ -2934,16 +3046,15 @@ dependencies = [ "rustc_version", "serde", "serde_json", - "sha2", "tokio", "xml-rs", ] [[package]] name = "rusoto_credential" -version = "0.43.0" +version = "0.45.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ba3e7cdf483d7198d9bca7414746d3ba656239e89e467b715d0571912f0b492f" +checksum = "09ac05563f83489b19b4d413607a30821ab08bbd9007d14fa05618da3ef09d8b" dependencies = [ "async-trait", "chrono", @@ -2961,12 +3072,12 @@ dependencies = [ [[package]] name = "rusoto_s3" -version = "0.43.0" +version = "0.45.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2b6bc3221ae5a2c036d5757eee68a2ffb6b7f87b8a83adbf4271c8133fdee01c" +checksum = "1146e37a7c1df56471ea67825fe09bbbd37984b5f6e201d8b2e0be4ee15643d8" dependencies = [ "async-trait", - "bytes", + "bytes 0.5.6", "futures 0.3.8", "rusoto_core", "xml-rs", @@ -2974,12 +3085,12 @@ dependencies = [ [[package]] name = "rusoto_signature" -version = "0.43.0" +version = "0.45.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "62940a2bd479900a1bf8935b8f254d3e19368ac3ac4570eb4bd48eb46551a1b7" +checksum = "97a740a88dde8ded81b6f2cff9cd5e054a5a2e38a38397260f7acdd2c85d17dd" dependencies = [ "base64 0.12.3", - "bytes", + "bytes 0.5.6", "futures 0.3.8", "hex", "hmac", @@ -2999,14 +3110,14 @@ dependencies = [ [[package]] name = "rust-argon2" -version = "0.8.2" +version = "0.8.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9dab61250775933275e84053ac235621dfb739556d5c54a2f2e9313b7cf43a19" +checksum = "4b18820d944b33caa75a71378964ac46f58517c92b6ae5f762636247c09e78fb" dependencies = [ - "base64 0.12.3", + "base64 0.13.0", "blake2b_simd", "constant_time_eq", - "crossbeam-utils", + "crossbeam-utils 0.8.1", ] [[package]] @@ -3142,18 +3253,18 @@ checksum = "388a1df253eca08550bef6c72392cfe7c30914bf41df5269b68cbd6ff8f570a3" [[package]] name = "serde" -version = "1.0.117" +version = "1.0.118" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b88fa983de7720629c9387e9f517353ed404164b1e482c970a90c1a4aaf7dc1a" +checksum = "06c64263859d87aa2eb554587e2d23183398d617427327cf2b3d0ed8c69e4800" dependencies = [ "serde_derive", ] [[package]] name = "serde_derive" -version = "1.0.117" +version = "1.0.118" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cbd1ae72adb44aab48f325a02444a5fc079349a8d804c1fc922aed3f7454c74e" +checksum = "c84d3526699cd55261af4b941e4e725444df67aa4f9e6a3564f18030d12672df" dependencies = [ "proc-macro2", "quote", @@ -3162,9 +3273,9 @@ dependencies = [ [[package]] name = "serde_json" -version = "1.0.59" +version = "1.0.60" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dcac07dbffa1c65e7f816ab9eba78eb142c6d44410f4eeba1e26e4f5dfa56b95" +checksum = "1500e84d27fe482ed1dc791a56eddc2f230046a040fa908c08bda1d9fb615779" dependencies = [ "itoa", "ryu", @@ -3183,6 +3294,18 @@ dependencies = [ "url", ] +[[package]] +name = "serde_urlencoded" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "edfa57a7f8d9c1d260a549e7224100f6c43d43f9103e06dd8b4095a9b2b43ce9" +dependencies = [ + "form_urlencoded", + "itoa", + "ryu", + "serde", +] + [[package]] name = "sha-1" version = "0.8.2" @@ -3216,14 +3339,15 @@ checksum = "2579985fda508104f7587689507983eadd6a6e84dd35d6d115361f530916fa0d" [[package]] name = "sha2" -version = "0.8.2" +version = "0.9.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a256f46ea78a0c0d9ff00077504903ac881a1dafdc20da66545699e7776b3e69" +checksum = "6e7aab86fe2149bad8c507606bdb3f4ef5e7b2380eb92350f56122cca72a42a8" dependencies = [ - "block-buffer 0.7.3", - "digest 0.8.1", - "fake-simd", - "opaque-debug 0.2.3", + "block-buffer 0.9.0", + "cfg-if 1.0.0", + "cpuid-bool", + "digest 0.9.0", + "opaque-debug 0.3.0", ] [[package]] @@ -3240,9 +3364,9 @@ checksum = "7fdf1b9db47230893d76faad238fd6097fd6d6a9245cd7a4d90dbd639536bbd2" [[package]] name = "signal-hook-registry" -version = "1.2.2" +version = "1.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ce32ea0c6c56d5eacaeb814fbed9960547021d3edd010ded1425f180536b20ab" +checksum = "16f1d0fef1604ba8f7a073c7e701f213e056707210e9020af4528e0101ce11a6" dependencies = [ "libc", ] @@ -3255,19 +3379,18 @@ checksum = "c111b5bd5695e56cffe5129854aa230b39c93a305372fdbb2668ca2394eea9f8" [[package]] name = "smallvec" -version = "1.5.0" +version = "1.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7acad6f34eb9e8a259d3283d1e8c1d34d7415943d4895f65cc73813c7396fc85" +checksum = "ae524f056d7d770e174287294f562e95044c68e88dec909a00d2094805db9d75" [[package]] name = "socket2" -version = "0.3.17" +version = "0.3.19" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2c29947abdee2a218277abeca306f25789c938e500ea5a9d4b12a5a504466902" +checksum = "122e570113d28d773067fab24266b66753f6ea915758651696b6e35e49f88d6e" dependencies = [ "cfg-if 1.0.0", "libc", - "redox_syscall", "winapi 0.3.9", ] @@ -3337,15 +3460,15 @@ checksum = "8ea5119cdb4c55b55d432abb513a0429384878c15dde60cc77b1c99de1a95a6a" [[package]] name = "subtle" -version = "1.0.0" +version = "2.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2d67a5a62ba6e01cb2192ff309324cb4875d0c451d55fe2319433abe7a05a8ee" +checksum = "1e81da0851ada1f3e9d4312c704aa4f8806f0f9d69faaf8df2f3464b4a9437c2" [[package]] name = "syn" -version = "1.0.50" +version = "1.0.55" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "443b4178719c5a851e1bde36ce12da21d74a0e60b4d982ec3385a933c812f0f6" +checksum = "a571a711dddd09019ccc628e1b17fe87c59b09d513c06c026877aa708334f37a" dependencies = [ "proc-macro2", "quote", @@ -3534,11 +3657,11 @@ checksum = "cda74da7e1a664f795bb1f8a87ec406fb89a02522cf6e50620d016add6dbbf5c" [[package]] name = "tokio" -version = "0.2.23" +version = "0.2.24" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a6d7ad61edd59bfcc7e80dababf0f4aed2e6d5e0ba1659356ae889752dfc12ff" +checksum = "099837d3464c16a808060bb3f02263b412f6fafcb5d01c533d309985fbeebe48" dependencies = [ - "bytes", + "bytes 0.5.6", "fnv", "futures-core", "iovec", @@ -3548,7 +3671,7 @@ dependencies = [ "mio", "mio-named-pipes", "mio-uds", - "pin-project-lite", + "pin-project-lite 0.1.11", "signal-hook-registry", "slab", "tokio-macros", @@ -3582,11 +3705,11 @@ version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "571da51182ec208780505a32528fc5512a8fe1443ab960b3f2f3ef093cd16930" dependencies = [ - "bytes", + "bytes 0.5.6", "futures-core", "futures-sink", "log", - "pin-project-lite", + "pin-project-lite 0.1.11", "tokio", ] @@ -3596,20 +3719,20 @@ version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "be8242891f2b6cbef26a2d7e8605133c2c554cd35b3e4948ea892d6d68436499" dependencies = [ - "bytes", + "bytes 0.5.6", "futures-core", "futures-io", "futures-sink", "log", - "pin-project-lite", + "pin-project-lite 0.1.11", "tokio", ] [[package]] name = "toml" -version = "0.5.7" +version = "0.5.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "75cf45bb0bef80604d001caaec0d09da99611b3c0fd39d3080468875cdb65645" +checksum = "a31142970826733df8241ef35dc040ef98c679ab14d7c3e54d827099b3acecaa" dependencies = [ "serde", ] @@ -3622,13 +3745,13 @@ checksum = "e987b6bf443f4b5b3b6f38704195592cca41c5bb7aedd3c3693c7081f8289860" [[package]] name = "tracing" -version = "0.1.21" +version = "0.1.22" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b0987850db3733619253fe60e17cb59b82d37c7e6c0236bb81e4d6b87c879f27" +checksum = "9f47026cdc4080c07e49b37087de021820269d996f581aac150ef9e5583eefe3" dependencies = [ - "cfg-if 0.1.10", + "cfg-if 1.0.0", "log", - "pin-project-lite", + "pin-project-lite 0.2.0", "tracing-core", ] @@ -3814,9 +3937,9 @@ dependencies = [ [[package]] name = "unicode-segmentation" -version = "1.7.0" +version = "1.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "db8716a166f290ff49dabc18b44aa407cb7c6dbe1aa0971b44b8a24b0ca35aae" +checksum = "bb0d2e7be6ae3a5fa87eed5fb451aff96f2573d2694942e40543ae0bbe19c796" [[package]] name = "unicode-width" @@ -3879,9 +4002,9 @@ dependencies = [ [[package]] name = "v_escape" -version = "0.13.2" +version = "0.14.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "039a44473286eb84e4e74f90165feff67c802dbeced7ee4c5b00d719b0d0475e" +checksum = "ccca9e73c678b882900cbaec16dae4d3662ace5a17774ac45af04e0f3988fafa" dependencies = [ "buf-min", "v_escape_derive", @@ -3901,9 +4024,9 @@ dependencies = [ [[package]] name = "v_htmlescape" -version = "0.10.4" +version = "0.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "11d7c2a33ed7cf0dc1b42bcf39e01b6512f9df08f09e1cd8a49d9dc49a6a9482" +checksum = "db00c903248abee8499af60bf20d242e7882335bbbffd2614915184cbb207402" dependencies = [ "cfg-if 1.0.0", "v_escape", @@ -3911,9 +4034,9 @@ dependencies = [ [[package]] name = "vcpkg" -version = "0.2.10" +version = "0.2.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6454029bf181f092ad1b853286f23e2c507d8e8194d01d92da4a55c274a5508c" +checksum = "b00bca6106a5e23f3eee943593759b7fcddb00554332e856d990c893966879fb" [[package]] name = "vec_map" @@ -3968,11 +4091,11 @@ checksum = "1a143597ca7c7793eff794def352d41792a93c481eb1042423ff7ff72ba2c31f" [[package]] name = "wasm-bindgen" -version = "0.2.68" +version = "0.2.69" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1ac64ead5ea5f05873d7c12b545865ca2b8d28adfc50a49b84770a3a97265d42" +checksum = "3cd364751395ca0f68cafb17666eee36b63077fb5ecd972bbcd74c90c4bf736e" dependencies = [ - "cfg-if 0.1.10", + "cfg-if 1.0.0", "serde", "serde_json", "wasm-bindgen-macro", @@ -3980,9 +4103,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-backend" -version = "0.2.68" +version = "0.2.69" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f22b422e2a757c35a73774860af8e112bff612ce6cb604224e8e47641a9e4f68" +checksum = "1114f89ab1f4106e5b55e688b828c0ab0ea593a1ea7c094b141b14cbaaec2d62" dependencies = [ "bumpalo", "lazy_static", @@ -3995,11 +4118,11 @@ dependencies = [ [[package]] name = "wasm-bindgen-futures" -version = "0.4.18" +version = "0.4.19" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b7866cab0aa01de1edf8b5d7936938a7e397ee50ce24119aef3e1eaa3b6171da" +checksum = "1fe9756085a84584ee9457a002b7cdfe0bfff169f45d2591d8be1345a6780e35" dependencies = [ - "cfg-if 0.1.10", + "cfg-if 1.0.0", "js-sys", "wasm-bindgen", "web-sys", @@ -4007,9 +4130,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro" -version = "0.2.68" +version = "0.2.69" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6b13312a745c08c469f0b292dd2fcd6411dba5f7160f593da6ef69b64e407038" +checksum = "7a6ac8995ead1f084a8dea1e65f194d0973800c7f571f6edd70adf06ecf77084" dependencies = [ "quote", "wasm-bindgen-macro-support", @@ -4017,9 +4140,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro-support" -version = "0.2.68" +version = "0.2.69" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f249f06ef7ee334cc3b8ff031bfc11ec99d00f34d86da7498396dc1e3b1498fe" +checksum = "b5a48c72f299d80557c7c62e37e7225369ecc0c963964059509fbafe917c7549" dependencies = [ "proc-macro2", "quote", @@ -4030,15 +4153,15 @@ dependencies = [ [[package]] name = "wasm-bindgen-shared" -version = "0.2.68" +version = "0.2.69" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1d649a3145108d7d3fbcde896a468d1bd636791823c9921135218ad89be08307" +checksum = "7e7811dd7f9398f14cc76efd356f98f03aa30419dea46aa810d71e819fc97158" [[package]] name = "wasm-bindgen-test" -version = "0.3.18" +version = "0.3.19" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "34d1cdc8b98a557f24733d50a1199c4b0635e465eecba9c45b214544da197f64" +checksum = "0355fa0c1f9b792a09b6dcb6a8be24d51e71e6d74972f9eb4a44c4c004d24a25" dependencies = [ "console_error_panic_hook", "js-sys", @@ -4050,24 +4173,87 @@ dependencies = [ [[package]] name = "wasm-bindgen-test-macro" -version = "0.3.18" +version = "0.3.19" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e8fb9c67be7439ee8ab1b7db502a49c05e51e2835b66796c705134d9b8e1a585" +checksum = "27e07b46b98024c2ba2f9e83a10c2ef0515f057f2da299c1762a2017de80438b" dependencies = [ "proc-macro2", "quote", ] +[[package]] +name = "web-actor" +version = "0.1.0" +dependencies = [ + "actix 0.10.0", + "actix-cors", + "actix-multipart", + "actix-rt", + "actix-service", + "actix-web", + "actix-web-actors", + "bincode", + "bytes 0.5.6", + "database-actor", + "env_logger", + "filesystem-actor", + "flate2", + "futures 0.3.8", + "jirs-config", + "jirs-data", + "lazy_static", + "libc", + "log", + "mail-actor", + "openssl-sys", + "pretty_env_logger", + "rusoto_core", + "rusoto_s3", + "rusoto_signature", + "serde", + "syntect", + "tokio", + "toml", + "uuid 0.8.1", + "websocket-actor", +] + [[package]] name = "web-sys" -version = "0.3.45" +version = "0.3.46" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4bf6ef87ad7ae8008e15a355ce696bed26012b7caa21605188cfd8214ab51e2d" +checksum = "222b1ef9334f92a21d3fb53dc3fd80f30836959a90f9274a626d7e06315ba3c3" dependencies = [ "js-sys", "wasm-bindgen", ] +[[package]] +name = "websocket-actor" +version = "0.1.0" +dependencies = [ + "actix 0.10.0", + "actix-web", + "actix-web-actors", + "bincode", + "database-actor", + "env_logger", + "flate2", + "futures 0.3.8", + "jirs-config", + "jirs-data", + "lazy_static", + "libc", + "log", + "mail-actor", + "openssl-sys", + "pretty_env_logger", + "serde", + "syntect", + "toml", + "uuid 0.8.1", +] + [[package]] name = "wee_alloc" version = "0.4.5" @@ -4180,6 +4366,6 @@ dependencies = [ [[package]] name = "zeroize" -version = "1.1.1" +version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "05f33972566adbd2d3588b0491eb94b98b43695c4ef897903470ede4f3f5a28a" +checksum = "81a974bcdd357f0dca4d41677db03436324d45a4c9ed2d0b873a5a360ce41c36" diff --git a/Cargo.toml b/Cargo.toml index df6a63f7..4c14b9a2 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -13,6 +13,14 @@ members = [ "./jirs-cli", "./jirs-server", "./jirs-client", - "./jirs-data", "./jirs-css", + "./shared/jirs-config", + "./shared/jirs-data", + "./actors/highlight-actor", + "./actors/database-actor", + "./actors/database-actor/database_actor-derive", + "./actors/web-actor", + "./actors/websocket-actor", + "./actors/mail-actor", + "./actors/filesystem-actor" ] diff --git a/actors/database-actor/Cargo.toml b/actors/database-actor/Cargo.toml new file mode 100644 index 00000000..11905e87 --- /dev/null +++ b/actors/database-actor/Cargo.toml @@ -0,0 +1,59 @@ +[package] +name = "database-actor" +version = "0.1.0" +authors = ["Adrian Wozniak "] +edition = "2018" +description = "JIRS (Simplified JIRA in Rust) shared data types" +repository = "https://gitlab.com/adrian.wozniak/jirs" +license = "MPL-2.0" +#license-file = "../LICENSE" + +[lib] +name = "database_actor" +path = "./src/lib.rs" + +[dependencies] +serde = "*" +bincode = "*" +toml = { version = "*" } + +actix = { version = "0.10.0" } +actix-web = { version = "*" } + +futures = { version = "0.3.8" } +openssl-sys = { version = "*", features = ["vendored"] } +libc = { version = "0.2.0", default-features = false } + +pq-sys = { version = ">=0.3.0, <0.5.0" } +r2d2 = { version = ">= 0.8, < 0.9" } + +dotenv = { version = "*" } + +byteorder = "1.0" +chrono = { version = "0.4", features = ["serde"] } +time = { version = "0.1" } +url = { version = "2.1.0" } +percent-encoding = { version = "2.1.0" } +uuid = { version = "0.8.1", features = ["serde", "v4", "v5"] } +ipnetwork = { version = ">=0.12.2, <0.17.0" } +num-bigint = { version = ">=0.1.41, <0.3" } +num-traits = { version = "0.2" } +num-integer = { version = "0.1.32" } +bigdecimal = { version = ">= 0.0.10, <= 0.1.0" } +bitflags = { version = "1.0" } + +log = "0.4" +pretty_env_logger = "0.4" +env_logger = "0.7" + +[dependencies.jirs-config] +path = "../../shared/jirs-config" +features = ["database"] + +[dependencies.jirs-data] +path = "../../shared/jirs-data" +features = ["backend"] + +[dependencies.diesel] +version = "1.4.5" +features = ["unstable", "postgres", "numeric", "extras", "uuidv07"] diff --git a/actors/database-actor/database_actor-derive/Cargo.toml b/actors/database-actor/database_actor-derive/Cargo.toml new file mode 100644 index 00000000..9b74471c --- /dev/null +++ b/actors/database-actor/database_actor-derive/Cargo.toml @@ -0,0 +1,17 @@ +[package] +name = "database-actor-derive" +version = "0.1.0" +authors = ["Adrian Wozniak "] +edition = "2018" +description = "JIRS (Simplified JIRA in Rust) shared data types" +repository = "https://gitlab.com/adrian.wozniak/jirs" +license = "MPL-2.0" +#license-file = "../LICENSE" + +[lib] +name = "database_actor_derive" +path = "./src/lib.rs" +proc-macro = true + +[dependencies] + diff --git a/actors/database-actor/database_actor-derive/src/lib.rs b/actors/database-actor/database_actor-derive/src/lib.rs new file mode 100644 index 00000000..97b31177 --- /dev/null +++ b/actors/database-actor/database_actor-derive/src/lib.rs @@ -0,0 +1,27 @@ +extern crate proc_macro; + +use proc_macro::{TokenStream, TokenTree}; + +#[proc_macro_derive(DbMsg, attributes(query))] +pub fn db_msg(item: TokenStream) -> TokenStream { + let mut it = item.into_iter(); + if let Some(TokenTree::Ident(ident)) = it.next() { + if ident.to_string().as_str() != "pub" { + panic!("Expect to find keyword pub but was found {:?}", ident) + } + } else { + panic!("Expect to find keyword pub but nothing was found") + } + if let Some(TokenTree::Ident(ident)) = it.next() { + if ident.to_string().as_str() != "struct" { + panic!("Expect to find keyword struct but was found {:?}", ident) + } + } else { + panic!("Expect to find keyword struct but nothing was found") + } + let _name = it + .next() + .expect("Expect to struct name but nothing was found"); + + "".parse().unwrap() +} diff --git a/actors/database-actor/src/authorize_user.rs b/actors/database-actor/src/authorize_user.rs new file mode 100644 index 00000000..7b6bafdd --- /dev/null +++ b/actors/database-actor/src/authorize_user.rs @@ -0,0 +1,18 @@ +use { + crate::{db_find, tokens::FindAccessToken}, + diesel::prelude::*, + jirs_data::User, +}; + +db_find! { + AuthorizeUser, + msg => conn => users => { + let token = FindAccessToken { + token: msg.access_token, + } + .execute(conn)?; + users.find(token.user_id) + }, + User, + access_token => uuid::Uuid +} diff --git a/actors/database-actor/src/comments.rs b/actors/database-actor/src/comments.rs new file mode 100644 index 00000000..b6c9c665 --- /dev/null +++ b/actors/database-actor/src/comments.rs @@ -0,0 +1,51 @@ +use { + crate::{db_create, db_delete, db_load, db_update}, + diesel::prelude::*, + jirs_data::{Comment, CommentId, IssueId, UserId}, +}; + +db_load! { + LoadIssueComments, + msg => comments => comments.distinct_on(id).filter(issue_id.eq(msg.issue_id)), + Comment, + issue_id => IssueId +} + +db_create! { + CreateComment, + msg => comments => diesel::insert_into(comments).values(( + body.eq(msg.body), + user_id.eq(msg.user_id), + issue_id.eq(msg.issue_id), + )), + Comment, + issue_id => IssueId, + user_id => UserId, + body => String +} + +db_update! { + UpdateComment, + msg => comments => diesel::update( + comments + .filter(user_id.eq(msg.user_id)) + .find(msg.comment_id), + ) + .set(body.eq(msg.body)), + Comment, + comment_id => CommentId, + user_id => UserId, + body => String +} + +db_delete! { + DeleteComment, + msg => comments => diesel::delete( + comments + .filter(user_id.eq(msg.user_id)) + .find(msg.comment_id), + ), + Comment, + comment_id => CommentId, + user_id => UserId +} diff --git a/actors/database-actor/src/epics.rs b/actors/database-actor/src/epics.rs new file mode 100644 index 00000000..4200b6a6 --- /dev/null +++ b/actors/database-actor/src/epics.rs @@ -0,0 +1,48 @@ +use { + crate::{db_create, db_delete, db_load, db_update}, + diesel::prelude::*, + jirs_data::Epic, +}; + +db_load! { + LoadEpics, + msg => epics => epics.distinct_on(id).filter(project_id.eq(msg.project_id)), + Epic, + project_id => i32 +} + +db_create! { + CreateEpic, + msg => epics => diesel::insert_into(epics).values(( + name.eq(msg.name.as_str()), + user_id.eq(msg.user_id), + project_id.eq(msg.project_id), + )), + Epic, + user_id => i32, + project_id => i32, + name => String +} + +db_update! { + UpdateEpic, + msg => epics => diesel::update( + epics + .filter(project_id.eq(msg.project_id)) + .find(msg.epic_id), + ).set(name.eq(msg.name)), + Epic, + epic_id => i32, + project_id => i32, + name => String +} + +db_delete! { + DeleteEpic, + msg => epics => diesel::delete( + epics.filter(user_id.eq(msg.user_id)).find(msg.epic_id) + ), + Epic, + user_id => i32, + epic_id => i32 +} diff --git a/actors/database-actor/src/errors.rs b/actors/database-actor/src/errors.rs new file mode 100644 index 00000000..036398f6 --- /dev/null +++ b/actors/database-actor/src/errors.rs @@ -0,0 +1,64 @@ +use jirs_data::{EmailString, UsernameString}; + +#[derive(Debug)] +pub enum OperationError { + LoadCollection, + LoadSingle, + Create, + Update, + Delete, +} + +#[derive(Debug)] +pub enum ResourceKind { + Epic, + Invitation, + IssueAssignee, + IssueStatus, + Issue, + Message, + Project, + Token, + UserProject, + User, + Comment, +} + +#[derive(Debug)] +pub enum InvitationError { + InvitationRevoked, +} + +#[derive(Debug)] +pub enum TokenError { + FailedToDisable, +} + +#[derive(Debug)] +pub enum UserError { + TakenPair(UsernameString, EmailString), + InvalidPair(UsernameString, EmailString), + UpdateProfile, +} + +#[derive(Debug)] +pub enum IssueError { + BadListPosition, + NoIssueStatuses, +} + +#[derive(Debug)] +pub enum UserProjectError { + InviteHimself, +} + +#[derive(Debug)] +pub enum DatabaseError { + DatabaseConnectionLost, + GenericFailure(OperationError, ResourceKind), + Invitation(InvitationError), + Token(TokenError), + User(UserError), + Issue(IssueError), + UserProject(UserProjectError), +} diff --git a/actors/database-actor/src/invitations.rs b/actors/database-actor/src/invitations.rs new file mode 100644 index 00000000..3b44c91b --- /dev/null +++ b/actors/database-actor/src/invitations.rs @@ -0,0 +1,171 @@ +use { + crate::{ + db_create, db_delete, db_find, db_load, db_pool, db_update, + tokens::CreateBindToken, + users::{LookupUser, Register}, + DbExecutor, DbPooledConn, InvitationError, + }, + actix::{Handler, Message}, + diesel::prelude::*, + jirs_data::{ + EmailString, Invitation, InvitationId, InvitationState, InvitationToken, ProjectId, Token, + User, UserId, UserRole, UsernameString, + }, +}; + +db_find! { + FindByBindToken, + msg => invitations => invitations.filter(bind_token.eq(msg.token)), + Invitation, + token => InvitationToken +} + +db_load! { + ListInvitation, + msg => invitations => invitations + .filter(invited_by_id.eq(msg.user_id)) + .filter(state.ne(InvitationState::Accepted)) + .order_by(state.asc()) + .then_order_by(updated_at.desc()), + Invitation, + user_id => UserId +} + +db_create! { + CreateInvitation, + msg => invitations => 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), + )), + Invitation, + user_id => UserId, + project_id => ProjectId, + email => EmailString, + name => UsernameString, + role => UserRole +} + +db_delete! { + DeleteInvitation, + msg => invitations => diesel::delete(invitations).filter(id.eq(msg.id)), + Invitation, + id => InvitationId +} + +db_update! { + UpdateInvitationState, + msg => invitations => diesel::update(invitations) + .set(( + state.eq(msg.state), + updated_at.eq(chrono::Utc::now().naive_utc()), + )) + .filter(id.eq(msg.id)), + Invitation, + id => InvitationId, + state => InvitationState +} + +pub struct RevokeInvitation { + pub id: InvitationId, +} + +impl Message for RevokeInvitation { + type Result = Result<(), crate::DatabaseError>; +} + +impl Handler for DbExecutor { + type Result = Result<(), crate::DatabaseError>; + + fn handle(&mut self, msg: RevokeInvitation, _ctx: &mut Self::Context) -> Self::Result { + let conn = db_pool!(self); + UpdateInvitationState { + id: msg.id, + state: InvitationState::Revoked, + } + .execute(conn)?; + Ok(()) + } +} + +pub struct AcceptInvitation { + pub invitation_token: InvitationToken, +} + +impl AcceptInvitation { + pub fn execute(self, conn: &DbPooledConn) -> Result { + crate::Guard::new(conn)?.run::(|_guard| { + let invitation = crate::invitations::FindByBindToken { + token: self.invitation_token, + } + .execute(conn)?; + + if invitation.state == InvitationState::Revoked { + return Err(crate::DatabaseError::Invitation( + InvitationError::InvitationRevoked, + )); + } + + crate::invitations::UpdateInvitationState { + id: invitation.id, + state: InvitationState::Accepted, + } + .execute(conn)?; + + UpdateInvitationState { + id: invitation.id, + state: InvitationState::Accepted, + } + .execute(conn)?; + + match { + Register { + name: invitation.name.clone(), + email: invitation.email.clone(), + project_id: Some(invitation.project_id), + role: UserRole::User, + } + .execute(conn) + } { + Ok(_) => (), + Err(crate::DatabaseError::User(crate::UserError::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)?; + + crate::user_projects::CreateUserProject { + user_id: user.id, + project_id: invitation.project_id, + is_current: false, + is_default: false, + role: invitation.role, + } + .execute(conn)?; + + crate::tokens::FindUserId { user_id: user.id }.execute(conn) + }) + } +} + +impl Message for AcceptInvitation { + type Result = Result; +} + +impl Handler for DbExecutor { + type Result = Result; + + fn handle(&mut self, msg: AcceptInvitation, _ctx: &mut Self::Context) -> Self::Result { + let conn = db_pool!(self); + + msg.execute(conn) + } +} diff --git a/actors/database-actor/src/issue_assignees.rs b/actors/database-actor/src/issue_assignees.rs new file mode 100644 index 00000000..194de06f --- /dev/null +++ b/actors/database-actor/src/issue_assignees.rs @@ -0,0 +1,59 @@ +use { + crate::{db_create, db_delete, db_load, db_load_field}, + diesel::{expression::dsl::not, prelude::*}, + jirs_data::{IssueAssignee, IssueId, UserId}, +}; + +db_create! { + AsignMultiple, + msg => issue_assignees => { + use crate::models::CreateIssueAssigneeForm; + let AsignMultiple { user_ids, issue_id: i_id } = msg; + + diesel::insert_into(issue_assignees) + .values(user_ids.into_iter().map(|u_id| { + CreateIssueAssigneeForm { + user_id: u_id, + issue_id: i_id + } + }).collect::>()) + }, + IssueAssignee, + user_ids => Vec, + issue_id => IssueId +} + +db_load! { + LoadAssignees, + msg => issue_assignees => issue_assignees + .distinct_on(id) + .filter(issue_id.eq(msg.issue_id)), + IssueAssignee, + issue_id => IssueId +} + +db_load_field! { + LoadAssigneesIds, + UserId, + msg => issue_assignees => issue_assignees + .select(user_id) + .filter(issue_id.eq(msg.issue_id)), + IssueAssignee, + issue_id => IssueId +} + +db_delete! { + DeleteIssueAssignees, + msg => issue_assignees => diesel::delete(issue_assignees.filter(issue_id.eq(msg.issue_id))), + IssueAssignee, + issue_id => IssueId +} + +db_delete! { + DropIssueAssignees, + msg => issue_assignees => diesel::delete(issue_assignees) + .filter(not(user_id.eq_any(msg.user_ids)).and(issue_id.eq(msg.issue_id))), + IssueAssignee, + issue_id => IssueId, + user_ids => Vec +} diff --git a/actors/database-actor/src/issue_statuses.rs b/actors/database-actor/src/issue_statuses.rs new file mode 100644 index 00000000..18cfb587 --- /dev/null +++ b/actors/database-actor/src/issue_statuses.rs @@ -0,0 +1,55 @@ +use { + crate::{db_create, db_delete, db_load, db_update}, + diesel::prelude::*, + jirs_data::{IssueStatus, IssueStatusId, Position, ProjectId, TitleString}, +}; + +db_load! { + LoadIssueStatuses, + msg => issue_statuses => issue_statuses + .distinct_on(id) + .filter(project_id.eq(msg.project_id)), + IssueStatus, + project_id => ProjectId +} + +db_create! { + CreateIssueStatus, + msg => issue_statuses => diesel::insert_into(issue_statuses).values(( + project_id.eq(msg.project_id), + name.eq(msg.name), + position.eq(msg.position), + )), + IssueStatus, + project_id => ProjectId, + position => i32, + name => TitleString +} + +db_delete! { + DeleteIssueStatus, + msg => issue_statuses => diesel::delete(issue_statuses) + .filter(id.eq(msg.issue_status_id)) + .filter(project_id.eq(msg.project_id) + ), + IssueStatus, + project_id => ProjectId, + issue_status_id => IssueStatusId +} + +db_update! { + UpdateIssueStatus, + msg => issue_statuses => 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)), + IssueStatus, + issue_status_id => IssueStatusId, + project_id => ProjectId, + position => Position, + name => TitleString +} diff --git a/actors/database-actor/src/issues.rs b/actors/database-actor/src/issues.rs new file mode 100644 index 00000000..b7e033c0 --- /dev/null +++ b/actors/database-actor/src/issues.rs @@ -0,0 +1,228 @@ +use { + crate::{ + db_create_with_conn, db_delete_with_conn, db_find, db_load, db_update_with_conn, + models::Issue, + }, + diesel::{expression::sql_literal::sql, prelude::*}, + jirs_data::{IssueId, IssuePriority, IssueStatusId, IssueType, ProjectId, UserId}, +}; + +db_find! { + LoadIssue, + msg => issues => issues.filter(id.eq(msg.issue_id)).distinct(), + Issue, + issue_id => IssueId +} + +db_load! { + LoadProjectIssues, + msg => issues => issues.filter(project_id.eq(msg.project_id)).distinct(), + Issue, + project_id => ProjectId +} + +db_update_with_conn! { + UpdateIssue, + msg => conn => issues => { + if let Some(user_ids) = msg.user_ids { + crate::issue_assignees::DropIssueAssignees { + issue_id: msg.issue_id, + user_ids: user_ids.clone(), + } + .execute(conn)?; + + let existing: Vec = crate::issue_assignees::LoadAssigneesIds { + issue_id: msg.issue_id, + } + .execute(conn)?; + crate::issue_assignees::AsignMultiple { + issue_id: msg.issue_id, + user_ids: user_ids + .into_iter() + .filter(|u_id| !existing.contains(u_id)) + .collect::>(), + } + .execute(conn)?; + } + diesel::update(issues.find(msg.issue_id)).set(( + msg.title.map(|v| title.eq(v)), + msg.issue_type.map(|v| issue_type.eq(v)), + msg.issue_status_id.map(|v| issue_status_id.eq(v)), + msg.priority.map(|p| priority.eq(p)), + msg.list_position.map(|pos| list_position.eq(pos)), + msg.description.map(|desc| description.eq(desc)), + msg.description_text.map(|t| description_text.eq(t)), + msg.estimate.map(|v| estimate.eq(v)), + msg.time_spent.map(|v| time_spent.eq(v)), + msg.time_remaining.map(|v| time_remaining.eq(v)), + msg.project_id.map(|v| project_id.eq(v)), + msg.reporter_id.map(|v| reporter_id.eq(v)), + msg.epic_id.map(|v| epic_id.eq(v)), + updated_at.eq(chrono::Utc::now().naive_utc()), + )) + }, + Issue, + issue_id => i32, + title => Option, + issue_type => Option, + priority => Option, + list_position => Option, + description => Option, + description_text => Option, + estimate => Option, + time_spent => Option, + time_remaining => Option, + project_id => Option, + user_ids => Option>, + reporter_id => Option, + issue_status_id => Option, + epic_id => Option> +} + +db_delete_with_conn! { + DeleteIssue, + msg => conn => issues => { + crate::issue_assignees::DeleteIssueAssignees { issue_id: msg.issue_id } + .execute(conn)?; + diesel::delete(issues.find(msg.issue_id)) + }, + Issue, + issue_id => IssueId +} + +mod inner { + use { + crate::{db_create, models::Issue}, + diesel::prelude::*, + jirs_data::{IssuePriority, IssueStatusId, IssueType}, + }; + + db_create! { + CreateIssue, + msg => issues => diesel::insert_into(issues) + .values(( + title.eq(msg.title), + issue_type.eq(msg.issue_type), + issue_status_id.eq(msg.issue_status_id), + priority.eq(msg.priority), + list_position.eq(msg.list_position), + description.eq(msg.description), + description_text.eq(msg.description_text), + estimate.eq(msg.estimate), + time_spent.eq(msg.time_spent), + time_remaining.eq(msg.time_remaining), + reporter_id.eq(msg.reporter_id), + project_id.eq(msg.project_id), + epic_id.eq(msg.epic_id) + )) + .on_conflict_do_nothing(), + Issue, + title => String, + list_position => i32, + issue_type => IssueType, + issue_status_id => IssueStatusId, + priority => IssuePriority, + description => Option, + description_text => Option, + estimate => Option, + time_spent => Option, + time_remaining => Option, + project_id => jirs_data::ProjectId, + reporter_id => jirs_data::UserId, + epic_id => Option + } +} + +db_create_with_conn! { + CreateIssue, + msg => conn => issues => { + let pos = issues + .select(sql("COALESCE(max(list_position), 0) + 1")) + .get_result::(conn) + .map_err(|e| { + log::error!("resolve new issue position failed {}", e); + crate::DatabaseError::Issue(crate::IssueError::BadListPosition) + })?; + let i_s_id: IssueStatusId = if msg.issue_status_id == 0 { + crate::issue_statuses::LoadIssueStatuses { project_id: msg.project_id } + .execute(conn)? + .first() + .ok_or_else(|| crate::DatabaseError::Issue(crate::IssueError::NoIssueStatuses))? + .id + } else { + msg.issue_status_id + }; + let assign_users = msg.user_ids + .iter() + .cloned() + .filter(|u_id| *u_id != msg.reporter_id) + .collect::>(); + let issue = inner::CreateIssue { + title: msg.title, + list_position: pos, + issue_type: msg.issue_type, + issue_status_id: i_s_id, + priority: msg.priority, + description: msg.description, + description_text: msg.description_text, + estimate: msg.estimate, + time_spent: msg.time_spent, + time_remaining: msg.time_remaining, + project_id: msg.project_id, + reporter_id: msg.reporter_id, + epic_id: msg.epic_id, + }.execute(conn)?; + crate::issue_assignees::AsignMultiple { + issue_id: issue.id, + user_ids: assign_users, + }; + issues.find(issue.id) + }, + Issue, + title => String, + issue_type => IssueType, + issue_status_id => IssueStatusId, + priority => IssuePriority, + description => Option, + description_text => Option, + estimate => Option, + time_spent => Option, + time_remaining => Option, + project_id => jirs_data::ProjectId, + reporter_id => jirs_data::UserId, + user_ids => Vec, + epic_id => Option +} + +// impl Handler for DbExecutor { +// type Result = Result; +// +// fn handle(&mut self, msg: CreateIssue, ctx: &mut Self::Context) -> Self::Result { +// use crate::schema::issue_assignees::dsl; +// use crate::schema::issues::dsl::issues; +// +// let mut values = vec![]; +// for user_id in msg.user_ids.iter() { +// values.push(crate::models::CreateIssueAssigneeForm { +// issue_id: issue.id, +// user_id: *user_id, +// }); +// } +// if !msg.user_ids.contains(&msg.reporter_id) { +// values.push(crate::models::CreateIssueAssigneeForm { +// issue_id: issue.id, +// user_id: msg.reporter_id, +// }); +// } +// +// diesel::insert_into(dsl::issue_assignees) +// .values(values) +// .execute(conn) +// .map_err(|e| { +// log::error!("{:?}", e); +// crate::DatabaseError::DatabaseConnectionLost +// })?; +// +// Ok(issue) +// } +// } diff --git a/actors/database-actor/src/lib.rs b/actors/database-actor/src/lib.rs new file mode 100644 index 00000000..f3ad7906 --- /dev/null +++ b/actors/database-actor/src/lib.rs @@ -0,0 +1,109 @@ +#![recursion_limit = "256"] + +#[macro_use] +extern crate diesel; + +pub use errors::*; +use { + actix::{Actor, SyncContext}, + diesel::pg::PgConnection, + diesel::r2d2::{self, ConnectionManager}, +}; + +pub mod authorize_user; +pub mod comments; +pub mod epics; +pub mod errors; +pub mod invitations; +pub mod issue_assignees; +pub mod issue_statuses; +pub mod issues; +pub mod messages; +pub mod models; +pub mod prelude; +pub mod projects; +pub mod schema; +pub mod tokens; +pub mod user_projects; +pub mod users; + +pub type DbPool = r2d2::Pool>; +pub type DbPooledConn = r2d2::PooledConnection>; + +pub struct DbExecutor { + pub pool: DbPool, + pub config: jirs_config::database::Configuration, +} + +impl Actor for DbExecutor { + type Context = SyncContext; +} + +impl Default for DbExecutor { + fn default() -> Self { + Self { + pool: build_pool(), + config: jirs_config::database::Configuration::read(), + } + } +} + +pub fn build_pool() -> DbPool { + dotenv::dotenv().ok(); + let config = jirs_config::database::Configuration::read(); + + let manager = ConnectionManager::::new(config.database_url); + r2d2::Pool::builder() + .max_size(config.concurrency as u32) + .build(manager) + .unwrap_or_else(|e| panic!("Failed to create pool. {}", e)) +} + +pub trait SyncQuery { + type Result; + + fn handle(&self, pool: &DbPool) -> Self::Result; +} + +pub struct Guard<'l> { + conn: &'l crate::DbPooledConn, + tm: &'l diesel::connection::AnsiTransactionManager, +} + +impl<'l> Guard<'l> { + pub fn new(conn: &'l DbPooledConn) -> Result { + use diesel::{connection::TransactionManager, prelude::*}; + let tm = conn.transaction_manager(); + tm.begin_transaction(conn).map_err(|e| { + log::error!("{:?}", e); + crate::DatabaseError::DatabaseConnectionLost + })?; + Ok(Self { conn, tm }) + } + + pub fn run Result>( + &self, + f: F, + ) -> Result { + use diesel::connection::TransactionManager; + + let r = f(self); + match r { + Ok(r) => { + self.tm.commit_transaction(self.conn).map_err(|e| { + log::error!("{:?}", e); + crate::DatabaseError::DatabaseConnectionLost + })?; + Ok(r) + } + Err(e) => { + log::error!("{:?}", e); + self.tm.rollback_transaction(self.conn).map_err(|e| { + log::error!("{:?}", e); + crate::DatabaseError::DatabaseConnectionLost + })?; + Err(e) + } + } + } +} diff --git a/actors/database-actor/src/messages.rs b/actors/database-actor/src/messages.rs new file mode 100644 index 00000000..f739cbf3 --- /dev/null +++ b/actors/database-actor/src/messages.rs @@ -0,0 +1,69 @@ +use { + crate::{ + db_create_with_conn, db_delete, db_load, + users::{FindUser, LookupUser}, + }, + diesel::prelude::*, + jirs_data::{BindToken, Message, MessageId, MessageType, User, UserId}, +}; + +db_load! { + LoadMessages, + msg => messages => messages.filter(receiver_id.eq(msg.user_id)), + Message, + user_id => UserId +} + +db_delete! { + MarkMessageSeen, + msg => messages => diesel::delete( + messages.find(msg.message_id).filter(receiver_id.eq(msg.user_id)) + ), + Message, + user_id => UserId, + message_id => MessageId +} + +#[derive(Debug)] +pub enum CreateMessageReceiver { + Reference(UserId), + Lookup { name: String, email: String }, +} + +db_create_with_conn! { + CreateMessage, + msg => conn => messages => { + let user: User = match msg.receiver { + CreateMessageReceiver::Lookup { name, email } => { + LookupUser { name, email }.execute(conn)? + } + CreateMessageReceiver::Reference(user_id) => FindUser { user_id }.execute(conn)?, + }; + + diesel::insert_into(messages).values(( + receiver_id.eq(user.id), + sender_id.eq(msg.sender_id), + summary.eq(msg.summary), + description.eq(msg.description), + message_type.eq(msg.message_type), + hyper_link.eq(msg.hyper_link), + )) + }, + Message, + receiver => CreateMessageReceiver, + sender_id => UserId, + summary => String, + description => String, + message_type => MessageType, + hyper_link => String +} + +db_load! { + LookupMessagesByToken, + msg => messages => messages.filter( + hyper_link.eq(format!("#{}", msg.token)).and(receiver_id.eq(msg.user_id)), + ), + Message, + token => BindToken, + user_id => UserId +} diff --git a/jirs-server/src/models.rs b/actors/database-actor/src/models.rs similarity index 89% rename from jirs-server/src/models.rs rename to actors/database-actor/src/models.rs index 0a6c9324..b4eb09c9 100644 --- a/jirs-server/src/models.rs +++ b/actors/database-actor/src/models.rs @@ -1,126 +1,126 @@ -use chrono::NaiveDateTime; -use serde::{Deserialize, Serialize}; -use uuid::Uuid; - -use jirs_data::{ - EpicId, InvitationState, IssuePriority, IssueStatusId, IssueType, ProjectCategory, ProjectId, - TimeTracking, UserId, -}; - -use crate::schema::*; - -#[derive(Debug, Serialize, Deserialize, Queryable)] -pub struct Issue { - pub id: i32, - pub title: String, - pub issue_type: IssueType, - pub priority: IssuePriority, - pub list_position: i32, - pub description: Option, - pub description_text: Option, - pub estimate: Option, - pub time_spent: Option, - pub time_remaining: Option, - pub reporter_id: i32, - pub project_id: i32, - pub created_at: NaiveDateTime, - pub updated_at: NaiveDateTime, - pub issue_status_id: IssueStatusId, - pub epic_id: Option, -} - -impl Into for Issue { - fn into(self) -> jirs_data::Issue { - jirs_data::Issue { - id: self.id, - title: self.title, - issue_type: self.issue_type, - priority: self.priority, - list_position: self.list_position, - description: self.description, - description_text: self.description_text, - estimate: self.estimate, - time_spent: self.time_spent, - time_remaining: self.time_remaining, - reporter_id: self.reporter_id, - project_id: self.project_id, - created_at: self.created_at, - updated_at: self.updated_at, - issue_status_id: self.issue_status_id, - epic_id: self.epic_id, - - user_ids: vec![], - } - } -} - -#[derive(Debug, Serialize, Deserialize, Insertable)] -#[table_name = "issues"] -pub struct CreateIssueForm { - pub title: String, - pub issue_type: IssueType, - pub priority: IssuePriority, - pub list_position: i32, - pub description: Option, - pub description_text: Option, - pub estimate: Option, - pub time_spent: Option, - pub time_remaining: Option, - pub reporter_id: UserId, - pub project_id: ProjectId, - pub issue_status_id: IssueStatusId, - pub epic_id: Option, -} - -#[derive(Debug, Serialize, Deserialize, Insertable)] -#[table_name = "issue_assignees"] -pub struct CreateIssueAssigneeForm { - pub issue_id: i32, - pub user_id: i32, -} - -#[derive(Debug, Serialize, Deserialize, Insertable)] -#[table_name = "projects"] -pub struct UpdateProjectForm { - pub name: Option, - pub url: Option, - pub description: Option, - pub category: Option, - pub time_tracking: Option, -} - -#[derive(Debug, Serialize, Deserialize, Insertable)] -#[table_name = "projects"] -pub struct CreateProjectForm { - pub name: String, - pub url: String, - pub description: String, - pub category: ProjectCategory, -} - -#[derive(Debug, Serialize, Deserialize, Insertable)] -#[table_name = "users"] -pub struct UserForm { - pub name: String, - pub email: String, - pub avatar_url: Option, -} - -#[derive(Debug, Serialize, Deserialize, Insertable)] -#[table_name = "tokens"] -pub struct TokenForm { - pub user_id: i32, - pub access_token: Uuid, - pub refresh_token: Uuid, - pub bind_token: Option, -} - -#[derive(Debug, Serialize, Deserialize, Insertable)] -#[table_name = "invitations"] -pub struct InvitationForm { - pub name: String, - pub email: String, - pub state: InvitationState, - pub project_id: i32, - pub invited_by_id: i32, -} +use { + crate::schema::*, + chrono::NaiveDateTime, + jirs_data::{ + EpicId, InvitationState, IssuePriority, IssueStatusId, IssueType, ProjectCategory, + ProjectId, TimeTracking, UserId, + }, + serde::{Deserialize, Serialize}, + uuid::Uuid, +}; + +#[derive(Debug, Serialize, Deserialize, Queryable)] +pub struct Issue { + pub id: i32, + pub title: String, + pub issue_type: IssueType, + pub priority: IssuePriority, + pub list_position: i32, + pub description: Option, + pub description_text: Option, + pub estimate: Option, + pub time_spent: Option, + pub time_remaining: Option, + pub reporter_id: i32, + pub project_id: i32, + pub created_at: NaiveDateTime, + pub updated_at: NaiveDateTime, + pub issue_status_id: IssueStatusId, + pub epic_id: Option, +} + +impl Into for Issue { + fn into(self) -> jirs_data::Issue { + jirs_data::Issue { + id: self.id, + title: self.title, + issue_type: self.issue_type, + priority: self.priority, + list_position: self.list_position, + description: self.description, + description_text: self.description_text, + estimate: self.estimate, + time_spent: self.time_spent, + time_remaining: self.time_remaining, + reporter_id: self.reporter_id, + project_id: self.project_id, + created_at: self.created_at, + updated_at: self.updated_at, + issue_status_id: self.issue_status_id, + epic_id: self.epic_id, + + user_ids: vec![], + } + } +} + +#[derive(Debug, Serialize, Deserialize, Insertable)] +#[table_name = "issues"] +pub struct CreateIssueForm { + pub title: String, + pub issue_type: IssueType, + pub priority: IssuePriority, + pub list_position: i32, + pub description: Option, + pub description_text: Option, + pub estimate: Option, + pub time_spent: Option, + pub time_remaining: Option, + pub reporter_id: UserId, + pub project_id: ProjectId, + pub issue_status_id: IssueStatusId, + pub epic_id: Option, +} + +#[derive(Debug, Serialize, Deserialize, Insertable)] +#[table_name = "issue_assignees"] +pub struct CreateIssueAssigneeForm { + pub issue_id: i32, + pub user_id: i32, +} + +#[derive(Debug, Serialize, Deserialize, Insertable)] +#[table_name = "projects"] +pub struct UpdateProjectForm { + pub name: Option, + pub url: Option, + pub description: Option, + pub category: Option, + pub time_tracking: Option, +} + +#[derive(Debug, Serialize, Deserialize, Insertable)] +#[table_name = "projects"] +pub struct CreateProjectForm { + pub name: String, + pub url: String, + pub description: String, + pub category: ProjectCategory, +} + +#[derive(Debug, Serialize, Deserialize, Insertable)] +#[table_name = "users"] +pub struct UserForm { + pub name: String, + pub email: String, + pub avatar_url: Option, +} + +#[derive(Debug, Serialize, Deserialize, Insertable)] +#[table_name = "tokens"] +pub struct TokenForm { + pub user_id: i32, + pub access_token: Uuid, + pub refresh_token: Uuid, + pub bind_token: Option, +} + +#[derive(Debug, Serialize, Deserialize, Insertable)] +#[table_name = "invitations"] +pub struct InvitationForm { + pub name: String, + pub email: String, + pub state: InvitationState, + pub project_id: i32, + pub invited_by_id: i32, +} diff --git a/actors/database-actor/src/prelude.rs b/actors/database-actor/src/prelude.rs new file mode 100644 index 00000000..7450fcb9 --- /dev/null +++ b/actors/database-actor/src/prelude.rs @@ -0,0 +1,284 @@ +#[macro_export] +macro_rules! db_pool { + ($self: expr) => { + &$self.pool.get().map_err(|e| { + log::error!("{:?}", e); + $crate::DatabaseError::DatabaseConnectionLost + })? + }; + ($self: expr, $pool: expr) => { + &$pool.get().map_err(|e| { + log::error!("{:?}", e); + $crate::DatabaseError::DatabaseConnectionLost + })? + }; +} + +#[macro_export] +macro_rules! q { + ($q: expr) => {{ + let q = $q; + log::debug!( + "{}", + diesel::debug_query::(&q).to_string() + ); + q + }}; +} + +#[macro_export] +macro_rules! db_find { + ($action: ident, $self: ident => $conn: ident => $schema: ident => $q: expr, $resource: ident, $($field: ident => $ty: ty),+) => { + pub struct $action { + $(pub $field : $ty),+ + } + + impl $action { + pub fn execute(self, $conn: &$crate::DbPooledConn) -> Result<$resource, crate::DatabaseError> { + use crate::schema:: $schema ::dsl::*; + let $self = self; + $crate::q!($q) + .first($conn) + .map_err(|e| { + log::error!("{:?}", e); + $crate::DatabaseError::GenericFailure( + $crate::OperationError::LoadCollection, + $crate::ResourceKind::$resource, + ) + }) + } + } + + impl actix::Message for $action { + type Result = Result<$resource, $crate::DatabaseError>; + } + + impl actix::Handler<$action> for $crate::DbExecutor { + type Result = Result<$resource, $crate::DatabaseError>; + + fn handle(&mut self, msg: $action, _ctx: &mut Self::Context) -> Self::Result { + let $conn = $crate::db_pool!(self); + msg.execute($conn) + } + } + }; + ($action: ident, $self: ident => $schema: ident => $q: expr, $resource: ident, $($field: ident => $ty: ty),+) => { + $crate::db_find! { $action, $self => conn => $schema => $q, $resource, $($field => $ty),+ } + }; +} + +#[macro_export] +macro_rules! db_load { + ($action: ident, $self: ident => $schema: ident => $q: expr, $resource: ident, $($field: ident => $ty: ty),+) => { + pub struct $action { + $(pub $field : $ty),+ + } + + impl $action { + pub fn execute(self, conn: &$crate::DbPooledConn) -> Result, $crate::DatabaseError> { + use crate::schema:: $schema ::dsl::*; + let $self = self; + $crate::q!($q) + .load(conn) + .map_err(|e| { + log::error!("{:?}", e); + $crate::DatabaseError::GenericFailure( + $crate::OperationError::LoadCollection, + $crate::ResourceKind::$resource, + ) + }) + } + } + + impl actix::Message for $action { + type Result = Result, $crate::DatabaseError>; + } + + impl actix::Handler<$action> for $crate::DbExecutor { + type Result = Result, $crate::DatabaseError>; + + fn handle(&mut self, msg: $action, _ctx: &mut Self::Context) -> Self::Result { + let conn = $crate::db_pool!(self); + + msg.execute(conn) + } + } + }; +} + +#[macro_export] +macro_rules! db_load_field { + ($action: ident, $return_type: ident, $self: ident => $schema: ident => $q: expr, $resource: ident, $($field: ident => $ty: ty),+) => { + pub struct $action { + $(pub $field : $ty),+ + } + + impl $action { + pub fn execute(self, conn: &$crate::DbPooledConn) -> Result, $crate::DatabaseError> { + use crate::schema:: $schema ::dsl::*; + let $self = self; + $crate::q!($q) + .load(conn) + .map_err(|e| { + log::error!("{:?}", e); + $crate::DatabaseError::GenericFailure( + $crate::OperationError::LoadCollection, + $crate::ResourceKind::$resource, + ) + }) + } + } + + impl actix::Message for $action { + type Result = Result, $crate::DatabaseError>; + } + + impl actix::Handler<$action> for $crate::DbExecutor { + type Result = Result, $crate::DatabaseError>; + + fn handle(&mut self, msg: $action, _ctx: &mut Self::Context) -> Self::Result { + let conn = $crate::db_pool!(self); + + msg.execute(conn) + } + } + }; +} +#[macro_export] +macro_rules! db_create { + ($action: ident, $self: ident => $schema: ident => $q: expr, $resource: ident, $($field: ident => $ty: ty),+) => { + $crate::db_create_with_conn! { $action, $self => conn => $schema => $q, $resource, $($field => $ty),+ } + } +} + +#[macro_export] +macro_rules! db_create_with_conn { + ($action: ident, $self: ident => $conn: ident => $schema: ident => $q: expr, $resource: ident, $($field: ident => $ty: ty),+) => { + pub struct $action { + $(pub $field : $ty),+ + } + + impl $action { + pub fn execute(self, $conn: &$crate::DbPooledConn) -> Result<$resource, crate::DatabaseError> { + crate::Guard::new($conn)?.run(|_guard| { + use crate::schema:: $schema ::dsl::*; + let $self = self; + $crate::q!($q) + .get_result::<$resource>($conn) + .map_err(|e| { + log::error!("{:?}", e); + $crate::DatabaseError::GenericFailure( + $crate::OperationError::Create, + $crate::ResourceKind::$resource, + ) + }) + }) + } + } + + impl actix::Message for $action { + type Result = Result<$resource, $crate::DatabaseError>; + } + + impl actix::Handler<$action> for $crate::DbExecutor { + type Result = Result<$resource, $crate::DatabaseError>; + + fn handle(&mut self, msg: $action, _ctx: &mut Self::Context) -> Self::Result { + let $conn = $crate::db_pool!(self); + + msg.execute($conn) + } + } + }; +} +#[macro_export] +macro_rules! db_update { + ($action: ident, $self: ident => $schema: ident => $q: expr, $resource: ident, $($field: ident => $ty: ty),+) => { + $crate::db_update_with_conn! { $action, $self => conn => $schema => $q, $resource, $($field => $ty),+ } + }; +} + +#[macro_export] +macro_rules! db_update_with_conn { + ($action: ident, $self: ident => $conn: ident => $schema: ident => $q: expr, $resource: ident, $($field: ident => $ty: ty),+) => { + pub struct $action { + $(pub $field : $ty),+ + } + + impl $action { + pub fn execute(self, $conn: &$crate::DbPooledConn) -> Result<$resource, crate::DatabaseError> { + use crate::schema:: $schema ::dsl::*; + let $self = self; + $crate::q!($q) + .get_result::<$resource>($conn) + .map_err(|e| { + log::error!("{:?}", e); + $crate::DatabaseError::GenericFailure( + $crate::OperationError::Update, + $crate::ResourceKind::$resource, + ) + }) + } + } + + impl actix::Message for $action { + type Result = Result<$resource, $crate::DatabaseError>; + } + + impl actix::Handler<$action> for $crate::DbExecutor { + type Result = Result<$resource, $crate::DatabaseError>; + + fn handle(&mut self, msg: $action, _ctx: &mut Self::Context) -> Self::Result { + let $conn = $crate::db_pool!(self); + + msg.execute ( $conn ) + } + } + }; +} + +#[macro_export] +macro_rules! db_delete { + ($action: ident, $self: ident => $schema: ident => $q: expr, $resource: ident, $($field: ident => $ty: ty),+) => { + $crate::db_delete_with_conn! { $action, $self => conn => $schema => $q, $resource, $($field => $ty),+ } + }; +} + +#[macro_export] +macro_rules! db_delete_with_conn { + ($action: ident, $self: ident => $conn: ident => $schema: ident => $q: expr, $resource: ident, $($field: ident => $ty: ty),+) => { + pub struct $action { + $(pub $field : $ty),+ + } + + impl $action { + pub fn execute(self, $conn: &$crate::DbPooledConn) -> Result { + use $crate::schema:: $schema ::dsl::*; + let $self = self; + $crate::q!($q) + .execute($conn) + .map_err(|e| { + log::error!("{:?}", e); + $crate::DatabaseError::GenericFailure( + $crate::OperationError::Delete, + $crate::ResourceKind::$resource, + ) + }) + } + } + + impl actix::Message for $action { + type Result = Result; + } + + impl actix::Handler<$action> for $crate::DbExecutor { + type Result = Result; + + fn handle(&mut self, msg: $action, _ctx: &mut Self::Context) -> Self::Result { + let $conn = $crate::db_pool!(self); + + msg.execute($conn) + } + } + }; +} diff --git a/actors/database-actor/src/projects.rs b/actors/database-actor/src/projects.rs new file mode 100644 index 00000000..0e7703c6 --- /dev/null +++ b/actors/database-actor/src/projects.rs @@ -0,0 +1,97 @@ +use { + crate::{db_create_with_conn, db_find, db_load, db_update}, + diesel::prelude::*, + jirs_data::{NameString, Project, ProjectCategory, ProjectId, TimeTracking, UserId}, +}; + +db_find! { + LoadCurrentProject, + msg => projects => projects.find(msg.project_id), + Project, + project_id => ProjectId +} + +mod inner { + use { + crate::db_create, + diesel::prelude::*, + jirs_data::{NameString, Project, ProjectCategory, TimeTracking}, + }; + + db_create! { + CreateProject, + msg => projects => 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(crate::schema::projects::all_columns), + Project, + name => NameString, + url => Option, + description => Option, + category => Option, + time_tracking => Option + } +} + +db_create_with_conn! { + CreateProject, + msg => conn => projects => { + let p = inner::CreateProject { + name: msg.name, + url: msg.url, + description: msg.description, + category: msg.category, + time_tracking: msg.time_tracking, + }.execute(conn)?; + crate::issue_statuses::CreateIssueStatus { + project_id: p.id, + position: 0, + name: "TODO".to_string(), + } + .execute(conn)?; + projects.find(p.id) + }, + Project, + name => NameString, + url => Option, + description => Option, + category => Option, + time_tracking => Option +} + +db_update! { + UpdateProject, + msg => projects => 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)), + )), + Project, + project_id => ProjectId, + name => Option, + url => Option, + description => Option, + category => Option, + time_tracking => Option +} + +db_load! { + LoadProjects, + msg => projects => { + use crate::schema::user_projects::{dsl::{user_projects, user_id, project_id}}; + projects + .inner_join(user_projects.on(project_id.eq(id))) + .filter(user_id.eq(msg.user_id)) + .distinct_on(id) + .select(crate::schema::projects::all_columns) + }, + Project, + user_id => UserId +} diff --git a/jirs-server/src/schema.patch b/actors/database-actor/src/schema.patch similarity index 100% rename from jirs-server/src/schema.patch rename to actors/database-actor/src/schema.patch diff --git a/jirs-server/src/schema.rs b/actors/database-actor/src/schema.rs similarity index 100% rename from jirs-server/src/schema.rs rename to actors/database-actor/src/schema.rs diff --git a/actors/database-actor/src/tokens.rs b/actors/database-actor/src/tokens.rs new file mode 100644 index 00000000..f70eec79 --- /dev/null +++ b/actors/database-actor/src/tokens.rs @@ -0,0 +1,48 @@ +use { + crate::{db_create, db_find, db_update_with_conn}, + diesel::prelude::*, + jirs_data::{Token, UserId}, +}; + +db_find! { + FindUserId, + msg => tokens => tokens.filter(user_id.eq(msg.user_id)).order_by(id.desc()), + Token, + user_id => UserId +} + +db_find! { + FindBindToken, + msg => tokens => tokens.filter(bind_token.eq(Some(msg.token))), + Token, + token => uuid::Uuid +} + +db_update_with_conn! { + UseBindToken, + msg => conn => tokens => { + let token = FindBindToken { token: msg.token }.execute(conn)?; + diesel::update(tokens.find(token.id)).set(bind_token.eq(None as Option)) + }, + Token, + token => uuid::Uuid +} + +db_find! { + FindAccessToken, + msg => tokens => tokens.filter(access_token.eq(msg.token)), + Token, + token => uuid::Uuid +} + +db_create! { + CreateBindToken, + msg => tokens => diesel::insert_into(tokens).values(( + user_id.eq(msg.user_id), + access_token.eq(uuid::Uuid::new_v4()), + refresh_token.eq(uuid::Uuid::new_v4()), + bind_token.eq(Some(uuid::Uuid::new_v4())), + )), + Token, + user_id => UserId +} diff --git a/actors/database-actor/src/user_projects.rs b/actors/database-actor/src/user_projects.rs new file mode 100644 index 00000000..75370f77 --- /dev/null +++ b/actors/database-actor/src/user_projects.rs @@ -0,0 +1,134 @@ +use { + crate::{db_create, db_delete_with_conn, db_find, db_load, db_update_with_conn}, + diesel::prelude::*, + jirs_data::{ProjectId, UserId, UserProject, UserProjectId, UserRole}, +}; + +db_find! { + CurrentUserProject, + msg => user_projects => user_projects.filter(user_id.eq(msg.user_id).and(is_current.eq(true))), + UserProject, + user_id => UserId +} + +db_find! { + FindUserProject, + msg => user_projects => user_projects.filter(id.eq(msg.id).and(user_id.eq(msg.user_id))), + UserProject, + id => UserProjectId, + user_id => UserId +} + +db_load! { + LoadUserProjects, + msg => user_projects => user_projects.filter(user_id.eq(msg.user_id)), + UserProject, + user_id => UserId +} + +mod inner { + use { + crate::db_update, + diesel::prelude::*, + jirs_data::{UserId, UserProject, UserProjectId}, + }; + + db_update! { + ChangeProjectIsCurrent, + msg => user_projects => { + match msg.id { + Some(v) => diesel::update(user_projects.filter(user_id.eq(msg.user_id).and(id.eq(v)))).set(is_current.eq(msg.is_current)).into_boxed(), + _ => diesel::update(user_projects.filter(user_id.eq(msg.user_id))).set(is_current.eq(msg.is_current)).into_boxed(), + } + }, + UserProject, + id => Option, + user_id => UserId, + is_current => bool + } +} + +db_update_with_conn! { + ChangeCurrentUserProject, + msg => conn => user_projects => { + FindUserProject { + id: msg.id, + user_id: msg.user_id, + } + .execute(conn)?; + + inner::ChangeProjectIsCurrent { + id: None, + user_id: msg.user_id, + is_current: false, + } + .execute(conn)?; + + inner::ChangeProjectIsCurrent { + id: Some(msg.id), + user_id: msg.user_id, + is_current: false, + } + .execute(conn)?; + user_projects.find(msg.id) + }, + UserProject, + id => UserProjectId, + user_id => UserId +} + +db_find! { + FindByRole, + msg => user_projects => user_projects + .filter(user_id.eq(msg.user_id) + .and(project_id.eq(msg.project_id)) + .and(role.eq(msg.role) + ) + ), + UserProject, + user_id => UserId, + project_id => ProjectId, + role => UserRole +} + +db_delete_with_conn! { + RemoveInvitedUser, + msg => conn => user_projects => { + if msg.invited_id == msg.inviter_id { + return Err(crate::DatabaseError::UserProject(crate::UserProjectError::InviteHimself)); + } + FindByRole { + user_id: msg.inviter_id, + project_id: msg.project_id, + role: UserRole::Owner, + } + .execute(conn)?; + diesel::delete(user_projects) + .filter( + user_id.eq(msg.invited_id) + .and(project_id.eq(msg.project_id) + ) + ) + }, + UserProject, + invited_id => UserId, + inviter_id => UserId, + project_id => ProjectId +} + +db_create! { + CreateUserProject, + msg => user_projects => diesel::insert_into(user_projects).values(( + user_id.eq(msg.user_id), + project_id.eq(msg.project_id), + is_current.eq(msg.is_current), + is_default.eq(msg.is_default), + role.eq(msg.role), + )), + UserProject, + user_id => UserId, + project_id => ProjectId, + is_current => bool, + is_default => bool, + role => UserRole +} diff --git a/actors/database-actor/src/users.rs b/actors/database-actor/src/users.rs new file mode 100644 index 00000000..10cc5e48 --- /dev/null +++ b/actors/database-actor/src/users.rs @@ -0,0 +1,277 @@ +use { + crate::{ + db_create, db_create_with_conn, db_find, db_load, db_update, projects::CreateProject, q, + user_projects::CreateUserProject, DbPooledConn, + }, + diesel::prelude::*, + jirs_data::{EmailString, IssueId, ProjectId, User, UserId, UserRole, UsernameString}, +}; + +db_find! { + FindUser, + msg => users => users.find(msg.user_id), + User, + user_id => UserId +} + +db_find! { + LookupUser, + msg => users => users + .distinct_on(id) + .filter(email.eq(msg.email.as_str())) + .filter(name.eq(msg.name.as_str())), + User, + name => UsernameString, + email => EmailString +} + +db_load! { + LoadProjectUsers, + msg => users => { + use crate::schema::user_projects::dsl::{project_id, user_id, user_projects}; + use crate::schema::users::all_columns; + + users + .distinct_on(id) + .inner_join(user_projects.on(user_id.eq(id))) + .filter(project_id.eq(msg.project_id)) + .select(all_columns) + }, + User, + project_id => ProjectId +} + +db_load! { + LoadIssueAssignees, + msg => users => { + use crate::schema::issue_assignees::dsl::{issue_assignees, issue_id, user_id}; + users + .distinct_on(id) + .inner_join(issue_assignees.on(user_id.eq(id))) + .filter(issue_id.eq(msg.issue_id)) + .select(users::all_columns()) + }, + User, + issue_id => IssueId +} + +db_create! { + CreateUser, + msg => users => diesel::insert_into(users) + .values((name.eq(msg.name.as_str()), email.eq(msg.email.as_str()))), + User, + name => UsernameString, + email => EmailString +} + +/*impl CreateUser { + pub fn execute(self, conn: &DbPooledConn) -> Result { + 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| { + log::error!("{:?}", e); + let ws = match e { + Error::InvalidCString(_) => { + crate::DatabaseError::User(UserError::InvalidPair(self.name, self.email)) + } + Error::DatabaseError(diesel::result::DatabaseErrorKind::UniqueViolation, _) => { + crate::DatabaseError::User(UserError::TakenPair(self.name, self.email)) + } + Error::DatabaseError(_, _) => { + crate::DatabaseError::User(UserError::InvalidPair(self.name, self.email)) + } + Error::NotFound => { + crate::DatabaseError::User(UserError::InvalidPair(self.name, self.email)) + } + Error::QueryBuilderError(_) => { + crate::DatabaseError::User(UserError::InvalidPair(self.name, self.email)) + } + Error::DeserializationError(_) => { + crate::DatabaseError::User(UserError::InvalidPair(self.name, self.email)) + } + Error::SerializationError(_) => { + crate::DatabaseError::User(UserError::InvalidPair(self.name, self.email)) + } + Error::RollbackTransaction => { + crate::DatabaseError::User(UserError::InvalidPair(self.name, self.email)) + } + Error::AlreadyInTransaction => { + crate::DatabaseError::User(UserError::InvalidPair(self.name, self.email)) + } + Error::__Nonexhaustive => { + crate::DatabaseError::User(UserError::InvalidPair(self.name, self.email)) + } + }; + crate::DatabaseError::Error(ws) + }) + } +}*/ + +db_create_with_conn! { + Register, + msg => conn => users => { + if count_matching_users(msg.name.as_str(), msg.email.as_str(), conn) > 0 { + return Err(crate::DatabaseError::User(crate::UserError::InvalidPair(msg.name, msg.email))); + } + + let current_project_id: ProjectId = match msg.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: msg.name, + email: msg.email, + } + .execute(conn)?; + + CreateUserProject { + user_id: user.id, + project_id: current_project_id, + is_current: true, + is_default: true, + role: msg.role, + } + .execute(conn)?; + users.find(user.id) + }, + User, + name => UsernameString, + email => EmailString, + project_id => Option, + role => UserRole +} + +db_load! { + LoadInvitedUsers, + msg => users => { + use crate::schema::invitations::dsl::{email as i_email, invitations, invited_by_id}; + users + .inner_join(invitations.on(i_email.eq(email))) + .filter(invited_by_id.eq(msg.user_id)) + .select(users::all_columns()) + }, + User, + user_id => UserId +} + +fn count_matching_users(name: &str, email: &str, conn: &DbPooledConn) -> i64 { + use crate::schema::users::dsl; + + 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()) + .get_result::(conn) + .unwrap_or(1) +} + +db_update! { + UpdateAvatarUrl, + msg => users => diesel::update(users.find(msg.user_id)) + .set(avatar_url.eq(msg.avatar_url)), + User, + user_id => UserId, + avatar_url => Option +} + +db_update! { + ProfileUpdate, + msg => users => diesel::update(users.find(msg.user_id)) + .set((email.eq(msg.email), name.eq(msg.name))), + User, + user_id => UserId, + name => String, + email => String +} + +#[cfg(test)] +mod tests { + use diesel::connection::TransactionManager; + + use jirs_data::{Project, ProjectCategory}; + + use crate::build_pool; + + use super::*; + + #[test] + fn check_collision() { + use crate::schema::projects::dsl::projects; + use crate::schema::user_projects::dsl::user_projects; + use crate::schema::users::dsl::users; + + let pool = build_pool(); + let conn = &pool.get().unwrap(); + + let tm = conn.transaction_manager(); + + tm.begin_transaction(conn).unwrap(); + + diesel::delete(user_projects).execute(conn).unwrap(); + diesel::delete(users).execute(conn).unwrap(); + diesel::delete(projects).execute(conn).unwrap(); + + let project: Project = { + use crate::schema::projects::dsl::*; + + diesel::insert_into(projects) + .values(( + name.eq("baz".to_string()), + url.eq("/uz".to_string()), + description.eq("None".to_string()), + category.eq(ProjectCategory::Software), + )) + .get_result::(conn) + .unwrap() + }; + + let user: User = { + use crate::schema::users::dsl::*; + + diesel::insert_into(users) + .values(( + name.eq("Foo".to_string()), + email.eq("foo@example.com".to_string()), + )) + .get_result(conn) + .unwrap() + }; + { + use crate::schema::user_projects::dsl::*; + diesel::insert_into(user_projects) + .values(( + user_id.eq(user.id), + project_id.eq(project.id), + is_current.eq(true), + is_default.eq(true), + )) + .execute(conn) + .unwrap(); + } + + let res1 = count_matching_users("Foo", "bar@example.com", conn); + let res2 = count_matching_users("Bar", "foo@example.com", conn); + let res3 = count_matching_users("Foo", "foo@example.com", conn); + + tm.rollback_transaction(conn).unwrap(); + + assert_eq!(res1, 1); + assert_eq!(res2, 1); + assert_eq!(res3, 1); + } +} diff --git a/actors/filesystem-actor/Cargo.toml b/actors/filesystem-actor/Cargo.toml new file mode 100644 index 00000000..d2f5e8d3 --- /dev/null +++ b/actors/filesystem-actor/Cargo.toml @@ -0,0 +1,36 @@ +[package] +name = "filesystem-actor" +version = "0.1.0" +authors = ["Adrian Wozniak "] +edition = "2018" +description = "JIRS (Simplified JIRA in Rust) shared data types" +repository = "https://gitlab.com/adrian.wozniak/jirs" +license = "MPL-2.0" +#license-file = "../LICENSE" + +[lib] +name = "filesystem_actor" +path = "./src/lib.rs" + +[dependencies] +actix = { version = "0.10.0" } + +futures = { version = "0.3.8" } + +log = "0.4" +pretty_env_logger = "0.4" +env_logger = "0.7" + +bytes = { version = "0.5.6" } + +# Local storage +[dependencies.actix-files] +version = "*" + +[dependencies.jirs-config] +path = "../../shared/jirs-config" +features = ["local-storage"] + +[dependencies.tokio] +version = "0.2.23" +features = ["dns"] diff --git a/actors/filesystem-actor/src/lib.rs b/actors/filesystem-actor/src/lib.rs new file mode 100644 index 00000000..600ad8ad --- /dev/null +++ b/actors/filesystem-actor/src/lib.rs @@ -0,0 +1,81 @@ +use { + actix::SyncContext, + actix_files::{self, Files}, + jirs_config::fs::Configuration, + std::{io::Write, path::PathBuf}, +}; + +#[derive(Debug)] +pub enum FsError { + CopyFailed, + UnableToRemove, + CreateFile, + WriteFile, +} + +pub struct FileSystemExecutor { + config: Configuration, +} + +impl FileSystemExecutor { + pub fn client_path(&self) -> &str { + self.config.client_path.as_str() + } + + pub fn tmp_path(&self) -> &str { + self.config.tmp_path.as_str() + } +} + +impl Default for FileSystemExecutor { + fn default() -> Self { + Self { + config: Configuration::read(), + } + } +} + +impl actix::Actor for FileSystemExecutor { + type Context = SyncContext; +} + +#[derive(actix::Message)] +#[rtype(result = "Result")] +pub struct CreateFile { + pub source: tokio::sync::broadcast::Receiver, + pub file_name: String, +} + +impl actix::Handler for FileSystemExecutor { + type Result = Result; + + fn handle(&mut self, msg: CreateFile, _ctx: &mut Self::Context) -> Self::Result { + let Configuration { store_path, .. } = &self.config; + let CreateFile { + mut source, + file_name, + } = msg; + + let target = PathBuf::new().join(store_path).join(file_name); + let _ = std::fs::remove_file(&target); + let mut f = std::fs::File::create(target).map_err(|_| FsError::CreateFile)?; + + let count = futures::executor::block_on(async move { + let mut mem = 0; + while let Ok(b) = source.recv().await { + mem += f.write(&b).unwrap_or_default(); + } + mem + }); + Ok(count) + } +} + +pub fn service() -> Files { + let Configuration { + store_path, + client_path, + .. + } = Configuration::read(); + Files::new(client_path.as_str(), store_path.as_str()) +} diff --git a/actors/highlight-actor/Cargo.toml b/actors/highlight-actor/Cargo.toml new file mode 100644 index 00000000..b2d893ca --- /dev/null +++ b/actors/highlight-actor/Cargo.toml @@ -0,0 +1,36 @@ +[package] +name = "highlight-actor" +version = "0.1.0" +authors = ["Adrian Wozniak "] +edition = "2018" +description = "JIRS (Simplified JIRA in Rust) shared data types" +repository = "https://gitlab.com/adrian.wozniak/jirs" +license = "MPL-2.0" +#license-file = "../LICENSE" + +[lib] +name = "highlight_actor" +path = "./src/lib.rs" + +[dependencies] +serde = "*" +bincode = "*" +toml = { version = "*" } + +actix = { version = "0.10.0" } + +flate2 = { version = "*" } +syntect = { version = "*" } +lazy_static = { version = "*" } + +log = "0.4" +pretty_env_logger = "0.4" +env_logger = "0.7" + +[dependencies.jirs-config] +path = "../../shared/jirs-config" +features = ["hi"] + +[dependencies.jirs-data] +path = "../../shared/jirs-data" +features = ["backend"] diff --git a/actors/highlight-actor/src/lib.rs b/actors/highlight-actor/src/lib.rs new file mode 100644 index 00000000..b91a84d7 --- /dev/null +++ b/actors/highlight-actor/src/lib.rs @@ -0,0 +1,61 @@ +use { + actix::{Actor, Handler, SyncContext}, + std::sync::Arc, + syntect::{ + easy::HighlightLines, + highlighting::{Style, ThemeSet}, + parsing::SyntaxSet, + }, +}; + +mod load; + +lazy_static::lazy_static! { + pub static ref THEME_SET: Arc = Arc::new(load::integrated_themeset()); + pub static ref SYNTAX_SET: Arc = Arc::new(load::integrated_syntaxset()); +} + +#[derive(Debug)] +pub enum HighlightError { + UnknownLanguage, + UnknownTheme, + ResultUnserializable, +} + +fn hi<'l>(code: &'l str, lang: &'l str) -> Result, HighlightError> { + let set = SYNTAX_SET + .as_ref() + .find_syntax_by_name(lang) + .ok_or_else(|| HighlightError::UnknownLanguage)?; + let theme: &syntect::highlighting::Theme = THEME_SET + .as_ref() + .themes + .get("GitHub") + .ok_or_else(|| HighlightError::UnknownTheme)?; + + let mut hi = HighlightLines::new(set, theme); + Ok(hi.highlight(code, SYNTAX_SET.as_ref())) +} + +#[derive(Debug, Default)] +pub struct HighlightActor {} + +impl Actor for HighlightActor { + type Context = SyncContext; +} + +#[derive(actix::Message)] +#[rtype(result = "Result, HighlightError>")] +pub struct HighlightCode { + pub code: String, + pub lang: String, +} + +impl Handler for HighlightActor { + type Result = Result, HighlightError>; + + fn handle(&mut self, msg: HighlightCode, _ctx: &mut Self::Context) -> Self::Result { + let res = hi(&msg.code, &msg.lang)?; + bincode::serialize(&res).map_err(|_| HighlightError::ResultUnserializable) + } +} diff --git a/jirs-server/src/hi/load.rs b/actors/highlight-actor/src/load.rs similarity index 100% rename from jirs-server/src/hi/load.rs rename to actors/highlight-actor/src/load.rs diff --git a/jirs-server/src/hi/syntaxes.bin b/actors/highlight-actor/src/syntaxes.bin similarity index 100% rename from jirs-server/src/hi/syntaxes.bin rename to actors/highlight-actor/src/syntaxes.bin diff --git a/jirs-server/src/hi/themes.bin b/actors/highlight-actor/src/themes.bin similarity index 100% rename from jirs-server/src/hi/themes.bin rename to actors/highlight-actor/src/themes.bin diff --git a/actors/mail-actor/Cargo.toml b/actors/mail-actor/Cargo.toml new file mode 100644 index 00000000..122bcba3 --- /dev/null +++ b/actors/mail-actor/Cargo.toml @@ -0,0 +1,38 @@ +[package] +name = "mail-actor" +version = "0.1.0" +authors = ["Adrian Wozniak "] +edition = "2018" +description = "JIRS (Simplified JIRA in Rust) shared data types" +repository = "https://gitlab.com/adrian.wozniak/jirs" +license = "MPL-2.0" +#license-file = "../LICENSE" + +[lib] +name = "mail_actor" +path = "./src/lib.rs" + +[dependencies] +actix = { version = "0.10.0" } + +serde = "*" +toml = { version = "*" } + +log = "0.4" +pretty_env_logger = "0.4" +env_logger = "0.7" + +dotenv = { version = "*" } + +uuid = { version = "0.8.1", features = ["serde", "v4", "v5"] } + +futures = { version = "*" } +openssl-sys = { version = "*", features = ["vendored"] } +libc = { version = "0.2.0", default-features = false } + +lettre = { version = "*" } +lettre_email = { version = "*" } + +[dependencies.jirs-config] +path = "../../shared/jirs-config" +features = ["mail", "web"] diff --git a/jirs-server/src/mail/invite.rs b/actors/mail-actor/src/invite.rs similarity index 94% rename from jirs-server/src/mail/invite.rs rename to actors/mail-actor/src/invite.rs index c1a44b24..88b91ae5 100644 --- a/jirs-server/src/mail/invite.rs +++ b/actors/mail-actor/src/invite.rs @@ -3,7 +3,7 @@ use actix::{Handler, Message}; // use lettre_email; use uuid::Uuid; -use crate::mail::MailExecutor; +use crate::MailExecutor; #[derive(Debug)] pub struct Invite { @@ -23,7 +23,7 @@ impl Handler for MailExecutor { use lettre::Transport; let transport = &mut self.transport; let from = self.config.from.as_str(); - let addr = crate::web::Configuration::read().full_addr(); + let addr = jirs_config::web::Configuration::read().full_addr(); let html = format!( r#" diff --git a/actors/mail-actor/src/lib.rs b/actors/mail-actor/src/lib.rs new file mode 100644 index 00000000..797c3188 --- /dev/null +++ b/actors/mail-actor/src/lib.rs @@ -0,0 +1,46 @@ +use actix::{Actor, SyncContext}; + +// use lettre; + +pub mod invite; +pub mod welcome; + +pub type MailTransport = lettre::SmtpTransport; + +pub struct MailExecutor { + pub transport: MailTransport, + pub config: jirs_config::mail::Configuration, +} + +impl Actor for MailExecutor { + type Context = SyncContext; +} + +impl Default for MailExecutor { + fn default() -> Self { + let config = jirs_config::mail::Configuration::read(); + Self { + transport: mail_transport(&config), + config, + } + } +} + +fn mail_client(config: &jirs_config::mail::Configuration) -> lettre::SmtpClient { + let mail_user = config.user.as_str(); + let mail_pass = config.pass.as_str(); + let mail_host = config.host.as_str(); + + lettre::SmtpClient::new_simple(mail_host) + .expect("Failed to init SMTP client") + .credentials(lettre::smtp::authentication::Credentials::new( + mail_user.to_string(), + mail_pass.to_string(), + )) + .connection_reuse(lettre::smtp::ConnectionReuseParameters::ReuseUnlimited) + .smtp_utf8(true) +} + +fn mail_transport(config: &jirs_config::mail::Configuration) -> MailTransport { + mail_client(config).transport() +} diff --git a/jirs-server/src/mail/welcome.rs b/actors/mail-actor/src/welcome.rs similarity index 98% rename from jirs-server/src/mail/welcome.rs rename to actors/mail-actor/src/welcome.rs index bc63586c..1690f7cb 100644 --- a/jirs-server/src/mail/welcome.rs +++ b/actors/mail-actor/src/welcome.rs @@ -3,7 +3,7 @@ use actix::{Handler, Message}; // use lettre_email; use uuid::Uuid; -use crate::mail::MailExecutor; +use crate::MailExecutor; #[derive(Debug)] pub struct Welcome { diff --git a/actors/web-actor/Cargo.toml b/actors/web-actor/Cargo.toml new file mode 100644 index 00000000..dad732c4 --- /dev/null +++ b/actors/web-actor/Cargo.toml @@ -0,0 +1,85 @@ +[package] +name = "web-actor" +version = "0.1.0" +authors = ["Adrian Wozniak "] +edition = "2018" +description = "JIRS (Simplified JIRA in Rust) shared data types" +repository = "https://gitlab.com/adrian.wozniak/jirs" +license = "MPL-2.0" +#license-file = "../LICENSE" + +[lib] +name = "web_actor" +path = "./src/lib.rs" + +[features] +local-storage = ["filesystem-actor"] +aws-s3 = ["rusoto_s3", "rusoto_core"] +default = ["local-storage", "aws-s3"] + +[dependencies] +serde = "*" +bincode = "*" +toml = { version = "*" } + +actix = { version = "0.10.0" } +actix-web = { version = "*" } +actix-cors = { version = "*" } +actix-service = { version = "*" } +actix-rt = "1" +actix-web-actors = "*" +actix-multipart = "*" + +bytes = { version = "0.5.6" } + +futures = { version = "0.3.8" } +openssl-sys = { version = "*", features = ["vendored"] } +libc = { version = "0.2.0", default-features = false } + +flate2 = { version = "*" } +syntect = { version = "*" } +lazy_static = { version = "*" } + +log = "0.4" +pretty_env_logger = "0.4" +env_logger = "0.7" + +uuid = { version = "0.8.1", features = ["serde", "v4", "v5"] } + +[dependencies.jirs-config] +path = "../../shared/jirs-config" +features = ["mail", "web", "local-storage"] + +[dependencies.jirs-data] +path = "../../shared/jirs-data" +features = ["backend"] + +[dependencies.database-actor] +path = "../database-actor" + +[dependencies.mail-actor] +path = "../mail-actor" + +[dependencies.websocket-actor] +path = "../websocket-actor" + +[dependencies.filesystem-actor] +path = "../filesystem-actor" +optional = true + +# Amazon S3 +[dependencies.rusoto_s3] +optional = true +version = "0.45.0" + +[dependencies.rusoto_core] +optional = true +version = "0.45.0" + +[dependencies.rusoto_signature] +optional = true +version = "0.45.0" + +[dependencies.tokio] +version = "0.2.23" +features = ["dns"] diff --git a/actors/web-actor/src/avatar.rs b/actors/web-actor/src/avatar.rs new file mode 100644 index 00000000..7810606e --- /dev/null +++ b/actors/web-actor/src/avatar.rs @@ -0,0 +1,127 @@ +use std::io::Write; + +#[cfg(feature = "local-storage")] +use filesystem_actor; +use { + actix::Addr, + actix_multipart::{Field, Multipart}, + actix_web::{http::header::ContentDisposition, post, web, web::Data, Error, HttpResponse}, + database_actor::{ + authorize_user::AuthorizeUser, user_projects::CurrentUserProject, users::UpdateAvatarUrl, + DbExecutor, + }, + futures::{executor::block_on, StreamExt, TryStreamExt}, + jirs_data::{User, UserId, WsMsg}, + websocket_actor::server::{InnerMsg::BroadcastToChannel, WsServer}, +}; + +#[post("/")] +pub async fn upload( + mut payload: Multipart, + db: Data>, + ws: Data>, + fs: Data>, +) -> Result { + let mut user_id: Option = None; + let mut avatar_url: Option = None; + + while let Ok(Some(field)) = payload.try_next().await { + let disposition: ContentDisposition = match field.content_disposition() { + Some(d) => d, + _ => continue, + }; + if !disposition.is_form_data() { + return Ok(HttpResponse::BadRequest().finish()); + } + match disposition.get_name() { + Some("token") => { + user_id = Some(handle_token(field, db.clone()).await?); + } + Some("avatar") => { + let id = user_id.ok_or_else(|| HttpResponse::Unauthorized().finish())?; + avatar_url = Some( + crate::handlers::upload_avatar_image::handle_image( + id, + field, + disposition, + fs.clone(), + ) + .await?, + ); + } + _ => continue, + }; + } + let user_id = match user_id { + Some(id) => id, + _ => return Ok(HttpResponse::Unauthorized().finish()), + }; + + let project_id = match block_on(db.send(CurrentUserProject { user_id })) { + Ok(Ok(user_project)) => user_project.project_id, + _ => return Ok(HttpResponse::UnprocessableEntity().finish()), + }; + + match (user_id, avatar_url) { + (user_id, Some(avatar_url)) => { + let user = update_user_avatar(user_id, avatar_url.clone(), db).await?; + ws.send(BroadcastToChannel( + project_id, + WsMsg::AvatarUrlChanged(user.id, avatar_url), + )) + .await + .map_err(|_| HttpResponse::UnprocessableEntity().finish())?; + Ok(HttpResponse::NoContent().finish()) + } + _ => Ok(HttpResponse::UnprocessableEntity().finish()), + } +} + +async fn update_user_avatar( + user_id: UserId, + new_url: String, + db: Data>, +) -> Result { + match db + .send(UpdateAvatarUrl { + user_id, + avatar_url: Some(new_url), + }) + .await + { + Ok(Ok(user)) => Ok(user), + + Ok(Err(e)) => { + error!("{:?}", e); + Err(HttpResponse::Unauthorized().finish().into()) + } + Err(e) => { + error!("{:?}", e); + Err(HttpResponse::Unauthorized().finish().into()) + } + } +} + +async fn handle_token(mut field: Field, db: Data>) -> Result { + let mut f: Vec = vec![]; + while let Some(chunk) = field.next().await { + let data = chunk.unwrap(); + f = web::block(move || f.write_all(&data).map(|_| f)).await?; + } + let access_token = String::from_utf8(f) + .unwrap_or_default() + .parse::() + .map_err(|_| HttpResponse::Unauthorized().finish())?; + match db.send(AuthorizeUser { access_token }).await { + Ok(Ok(user)) => Ok(user.id), + + Ok(Err(e)) => { + error!("{:?}", e); + Err(HttpResponse::Unauthorized().finish().into()) + } + Err(e) => { + error!("{:?}", e); + Err(HttpResponse::Unauthorized().finish().into()) + } + } +} diff --git a/actors/web-actor/src/errors.rs b/actors/web-actor/src/errors.rs new file mode 100644 index 00000000..a41345bc --- /dev/null +++ b/actors/web-actor/src/errors.rs @@ -0,0 +1,74 @@ +use actix_web::HttpResponse; + +use jirs_data::{msg::WsError, ErrorResponse}; + +const TOKEN_NOT_FOUND: &str = "Token not found"; +const DATABASE_CONNECTION_FAILED: &str = "Database connection failed"; + +#[derive(Debug)] +pub enum HighlightError { + UnknownLanguage, + UnknownTheme, + ResultUnserializable, +} + +#[derive(Debug)] +pub enum ServiceError { + Unauthorized, + DatabaseConnectionLost, + DatabaseQueryFailed(String), + RecordNotFound(String), + RegisterCollision, + Error(WsError), + Highlight(HighlightError), +} + +impl ServiceError { + pub fn into_http_response(self) -> HttpResponse { + self.into() + } +} + +impl Into for ServiceError { + fn into(self) -> HttpResponse { + match self { + ServiceError::Unauthorized => HttpResponse::Unauthorized().json(ErrorResponse { + errors: vec![TOKEN_NOT_FOUND.to_owned()], + }), + ServiceError::DatabaseConnectionLost => { + HttpResponse::InternalServerError().json(ErrorResponse { + errors: vec![DATABASE_CONNECTION_FAILED.to_owned()], + }) + } + ServiceError::DatabaseQueryFailed(error) => { + HttpResponse::BadRequest().json(ErrorResponse { + errors: vec![error], + }) + } + ServiceError::RecordNotFound(resource_name) => { + HttpResponse::BadRequest().json(ErrorResponse { + errors: vec![format!("Resource not found {}", resource_name)], + }) + } + ServiceError::RegisterCollision => HttpResponse::Unauthorized().json(ErrorResponse { + errors: vec!["Register collision".to_string()], + }), + ServiceError::Error(error) => HttpResponse::BadRequest().json(ErrorResponse { + errors: vec![error.to_str().to_string()], + }), + ServiceError::Highlight(HighlightError::UnknownTheme) => HttpResponse::BadRequest() + .json(ErrorResponse::single( + "Code highlight Failed. Unexpected theme", + )), + ServiceError::Highlight(HighlightError::UnknownLanguage) => HttpResponse::BadRequest() + .json(ErrorResponse::single( + "Can't highlight in given language. It's unknown", + )), + ServiceError::Highlight(HighlightError::ResultUnserializable) => { + HttpResponse::BadRequest().json(ErrorResponse::single( + "Highlight succeed but result can't be send", + )) + } + } + } +} diff --git a/actors/web-actor/src/handlers/mod.rs b/actors/web-actor/src/handlers/mod.rs new file mode 100644 index 00000000..5afa7230 --- /dev/null +++ b/actors/web-actor/src/handlers/mod.rs @@ -0,0 +1 @@ +pub mod upload_avatar_image; diff --git a/actors/web-actor/src/handlers/upload_avatar_image.rs b/actors/web-actor/src/handlers/upload_avatar_image.rs new file mode 100644 index 00000000..0480a907 --- /dev/null +++ b/actors/web-actor/src/handlers/upload_avatar_image.rs @@ -0,0 +1,214 @@ +#[cfg(feature = "local-storage")] +use filesystem_actor::FileSystemExecutor; +use { + actix::Addr, + actix_multipart::Field, + actix_web::{http::header::ContentDisposition, web::Data, Error}, + futures::{StreamExt, TryStreamExt}, + jirs_data::UserId, + rusoto_core::ByteStream, + tokio::sync::broadcast::{Receiver, Sender}, +}; +#[cfg(feature = "aws-s3")] +use { + jirs_config::web::AmazonS3Storage, + rusoto_s3::{PutObjectRequest, S3Client, S3}, +}; + +#[cfg(all(feature = "local-storage", feature = "aws-s3"))] +pub(crate) async fn handle_image( + user_id: UserId, + mut field: Field, + disposition: ContentDisposition, + fs: Data>, +) -> Result { + let filename = disposition.get_filename().unwrap(); + let system_file_name = format!("{}-{}", user_id, filename); + + let (sender, receiver) = tokio::sync::broadcast::channel(4); + + let fs_fut = tokio::task::spawn(local_storage_write( + system_file_name.clone(), + fs.clone(), + user_id, + sender.subscribe(), + )); + + // Upload to AWS S3 + let aws_fut = tokio::task::spawn(aws_s3(system_file_name, receiver)); + + read_form_data(&mut field, sender).await; + + let mut new_link = None; + + if let Ok(url) = fs_fut.await { + new_link = url; + } + + if let Ok(url) = aws_fut.await { + new_link = url; + } + + Ok(new_link.unwrap_or_default()) +} + +#[cfg(all(not(feature = "local-storage"), feature = "aws-s3"))] +pub(crate) async fn handle_image( + user_id: UserId, + mut field: Field, + disposition: ContentDisposition, + fs: Data>, +) -> Result { + let filename = disposition.get_filename().unwrap(); + let system_file_name = format!("{}-{}", user_id, filename); + + let (sender, receiver) = tokio::sync::broadcast::channel(4); + + // Upload to AWS S3 + let aws_fut = aws_s3(system_file_name, receiver); + + read_form_data(&mut field, sender).await; + + let new_link = tokio::select! { + b = aws_fut => b, + }; + + { + use filesystem_actor::RemoveTmpFile; + let _ = fs + .send(RemoveTmpFile { + file_name: format!("{}-{}", user_id, filename), + }) + .await + .ok(); + }; + Ok(new_link.unwrap_or_default()) +} + +#[cfg(all(feature = "local-storage", not(feature = "aws-s3")))] +pub(crate) async fn handle_image( + user_id: UserId, + mut field: Field, + disposition: ContentDisposition, + fs: Data>, +) -> Result { + let filename = disposition.get_filename().unwrap(); + let system_file_name = format!("{}-{}", user_id, filename); + + let (sender, receiver) = tokio::sync::broadcast::channel(4); + + let fs_fut = local_storage_write( + system_file_name.clone(), + fs.clone(), + user_id, + sender.subscribe(), + ); + + read_form_data(&mut field, sender).await; + + let new_link = tokio::select! { + a = fs_fut => a, + }; + + { + use filesystem_actor::RemoveTmpFile; + let _ = fs + .send(RemoveTmpFile { + file_name: format!("{}-{}", user_id, filename), + }) + .await + .ok(); + }; + Ok(new_link.unwrap_or_default()) +} + +/// Read file from client +async fn read_form_data(field: &mut Field, sender: Sender) { + while let Some(chunk) = field.next().await { + let data = chunk.unwrap(); + if let Err(err) = sender.send(data) { + log::error!("{:?}", err); + } + } +} + +/// Stream bytes directly to AWS S3 Service +#[cfg(feature = "aws-s3")] +async fn aws_s3(system_file_name: String, mut receiver: Receiver) -> Option { + let web_config = jirs_config::web::Configuration::read(); + let s3 = &web_config.s3; + if !s3.active { + return None; + } + s3.set_variables(); + log::debug!("{:?}", s3); + + let mut v: Vec = vec![]; + use bytes::Buf; + + while let Ok(b) = receiver.recv().await { + v.extend_from_slice(b.bytes()) + } + // let stream = receiver.into_stream(); + // let stream = stream.map_err(|_e| std::io::Error::from_raw_os_error(1)); + + let client = S3Client::new(s3.region()); + let put_object = PutObjectRequest { + bucket: s3.bucket.clone(), + key: system_file_name.clone(), + // body: Some(ByteStream::new(stream)), + body: Some(v.into()), + ..Default::default() + }; + let id = match client.put_object(put_object).await { + Ok(obj) => obj, + Err(e) => { + log::error!("{}", e); + return None; + } + }; + log::debug!("{:?}", id); + Some(aws_s3_url(system_file_name.as_str(), s3)) +} + +/// +#[cfg(feature = "local-storage")] +async fn local_storage_write( + system_file_name: String, + fs: Data>, + user_id: jirs_data::UserId, + receiver: Receiver, +) -> Option { + let web_config = jirs_config::web::Configuration::read(); + let fs_config = jirs_config::fs::Configuration::read(); + + let _ = fs + .send(filesystem_actor::CreateFile { + source: receiver, + file_name: system_file_name.clone(), + }) + .await; + + Some(format!( + "{proto}://{bind}{port}{client_path}/{user_id}-{filename}", + proto = if web_config.ssl { "https" } else { "http" }, + bind = web_config.bind, + port = match web_config.port.as_str() { + "80" | "443" => "".to_string(), + p => format!(":{}", p), + }, + client_path = fs_config.client_path, + user_id = user_id, + filename = system_file_name + )) +} + +#[cfg(feature = "aws-s3")] +fn aws_s3_url(key: &str, config: &AmazonS3Storage) -> String { + format!( + "https://{bucket}.s3.{region}.amazonaws.com/{key}", + bucket = config.bucket, + region = config.region_name, + key = key + ) +} diff --git a/actors/web-actor/src/lib.rs b/actors/web-actor/src/lib.rs new file mode 100644 index 00000000..5a0ea0e5 --- /dev/null +++ b/actors/web-actor/src/lib.rs @@ -0,0 +1,36 @@ +#[macro_use] +extern crate log; + +pub use errors::*; +use { + crate::middleware::authorize::token_from_headers, + actix::Addr, + actix_web::{web::Data, HttpRequest, HttpResponse}, + database_actor::{authorize_user::AuthorizeUser, DbExecutor}, + jirs_data::User, +}; + +pub mod avatar; +pub mod errors; +pub mod handlers; +pub mod middleware; + +pub async fn user_from_request( + req: HttpRequest, + db: &Data>, +) -> Result { + let token = match token_from_headers(req.headers()) { + Ok(uuid) => uuid, + _ => return Err(ServiceError::Unauthorized.into_http_response()), + }; + match db + .send(AuthorizeUser { + access_token: token, + }) + .await + { + Ok(Ok(user)) => Ok(user), + Ok(Err(_e)) => Err(HttpResponse::InternalServerError().body("Critical database error")), + _ => Err(ServiceError::Unauthorized.into_http_response()), + } +} diff --git a/jirs-server/src/middleware/authorize.rs b/actors/web-actor/src/middleware/authorize.rs similarity index 81% rename from jirs-server/src/middleware/authorize.rs rename to actors/web-actor/src/middleware/authorize.rs index 711d11c3..0b67b584 100644 --- a/jirs-server/src/middleware/authorize.rs +++ b/actors/web-actor/src/middleware/authorize.rs @@ -1,16 +1,17 @@ -use std::task::{Context, Poll}; +use { + actix_service::{Service, Transform}, + actix_web::{ + dev::{ServiceRequest, ServiceResponse}, + http::header::{self}, + http::HeaderMap, + Error, + }, + futures::future::{ok, FutureExt, LocalBoxFuture, Ready}, + jirs_data::User, + std::task::{Context, Poll}, +}; -use actix_service::{Service, Transform}; -use actix_web::http::header::{self}; -use actix_web::http::HeaderMap; -use actix_web::{dev::ServiceRequest, dev::ServiceResponse, Error}; -use futures::future::{ok, FutureExt, LocalBoxFuture, Ready}; - -use jirs_data::User; - -use crate::db::SyncQuery; - -type Db = actix_web::web::Data; +type Db = actix_web::web::Data; #[derive(Default)] pub struct Authorize; @@ -97,8 +98,13 @@ fn check_token( pool: Db, ) -> std::result::Result { token_from_headers(headers).and_then(|access_token| { - use crate::db::authorize_user::AuthorizeUser; - AuthorizeUser { access_token }.handle(&pool) + use database_actor::authorize_user::AuthorizeUser; + let conn = pool + .get() + .map_err(|_| crate::errors::ServiceError::DatabaseConnectionLost)?; + AuthorizeUser { access_token } + .execute(&conn) + .map_err(|_| crate::errors::ServiceError::Unauthorized) }) } diff --git a/jirs-server/src/middleware/mod.rs b/actors/web-actor/src/middleware/mod.rs similarity index 100% rename from jirs-server/src/middleware/mod.rs rename to actors/web-actor/src/middleware/mod.rs diff --git a/actors/websocket-actor/Cargo.toml b/actors/websocket-actor/Cargo.toml new file mode 100644 index 00000000..e24eb52b --- /dev/null +++ b/actors/websocket-actor/Cargo.toml @@ -0,0 +1,50 @@ +[package] +name = "websocket-actor" +version = "0.1.0" +authors = ["Adrian Wozniak "] +edition = "2018" +description = "JIRS (Simplified JIRA in Rust) shared data types" +repository = "https://gitlab.com/adrian.wozniak/jirs" +license = "MPL-2.0" +#license-file = "../LICENSE" + +[lib] +name = "websocket_actor" +path = "./src/lib.rs" + +[dependencies] +serde = "*" +bincode = "*" +toml = { version = "*" } + +actix = { version = "0.10.0" } +actix-web = { version = "*" } +actix-web-actors = "*" + +futures = { version = "0.3.8" } +openssl-sys = { version = "*", features = ["vendored"] } +libc = { version = "0.2.0", default-features = false } + +flate2 = { version = "*" } +syntect = { version = "*" } +lazy_static = { version = "*" } + +log = "0.4" +pretty_env_logger = "0.4" +env_logger = "0.7" + +uuid = { version = "0.8.1", features = ["serde", "v4", "v5"] } + +[dependencies.jirs-config] +path = "../../shared/jirs-config" +features = ["websocket"] + +[dependencies.jirs-data] +path = "../../shared/jirs-data" +features = ["backend"] + +[dependencies.database-actor] +path = "../database-actor" + +[dependencies.mail-actor] +path = "../mail-actor" diff --git a/jirs-server/src/ws/auth.rs b/actors/websocket-actor/src/handlers/auth.rs similarity index 83% rename from jirs-server/src/ws/auth.rs rename to actors/websocket-actor/src/handlers/auth.rs index 7f90e2c8..0e0f23b9 100644 --- a/jirs-server/src/ws/auth.rs +++ b/actors/websocket-actor/src/handlers/auth.rs @@ -1,13 +1,15 @@ -use actix::AsyncContext; -use futures::executor::block_on; - -use jirs_data::{Token, WsMsg}; - -use crate::db::authorize_user::AuthorizeUser; -use crate::db::tokens::{CreateBindToken, FindBindToken}; -use crate::db::users::LookupUser; -use crate::mail::welcome::Welcome; -use crate::ws::{WebSocketActor, WsHandler, WsResult}; +use { + crate::{WebSocketActor, WsHandler, WsResult}, + actix::AsyncContext, + database_actor::{ + authorize_user::AuthorizeUser, + tokens::{CreateBindToken, FindBindToken}, + users::LookupUser, + }, + futures::executor::block_on, + jirs_data::{Token, WsMsg}, + mail_actor::welcome::Welcome, +}; pub struct Authenticate { pub name: String, @@ -21,22 +23,22 @@ impl WsHandler for WebSocketActor { let user = match block_on(self.db.send(LookupUser { name, email })) { Ok(Ok(user)) => user, Ok(Err(e)) => { - error!("{:?}", e); + log::error!("{:?}", e); return Ok(None); } Err(e) => { - error!("{:?}", e); + log::error!("{:?}", e); return Ok(None); } }; let token = match block_on(self.db.send(CreateBindToken { user_id: user.id })) { Ok(Ok(token)) => token, Ok(Err(e)) => { - error!("{:?}", e); + log::error!("{:?}", e); return Ok(None); } Err(e) => { - error!("{:?}", e); + log::error!("{:?}", e); return Ok(None); } }; @@ -47,11 +49,11 @@ impl WsHandler for WebSocketActor { })) { Ok(Ok(_)) => (), Ok(Err(e)) => { - error!("{}", e); + log::error!("{}", e); return Ok(None); } Err(e) => { - error!("{}", e); + log::error!("{}", e); return Ok(None); } } diff --git a/jirs-server/src/ws/comments.rs b/actors/websocket-actor/src/handlers/comments.rs similarity index 77% rename from jirs-server/src/ws/comments.rs rename to actors/websocket-actor/src/handlers/comments.rs index 1bc046ae..cdda53a2 100644 --- a/jirs-server/src/ws/comments.rs +++ b/actors/websocket-actor/src/handlers/comments.rs @@ -2,7 +2,7 @@ use futures::executor::block_on; use jirs_data::{CommentId, CreateCommentPayload, IssueId, UpdateCommentPayload, WsMsg}; -use crate::ws::{WebSocketActor, WsHandler, WsResult}; +use crate::{WebSocketActor, WsHandler, WsResult}; pub struct LoadIssueComments { pub issue_id: IssueId, @@ -12,16 +12,16 @@ impl WsHandler for WebSocketActor { fn handle_msg(&mut self, msg: LoadIssueComments, _ctx: &mut Self::Context) -> WsResult { self.require_user()?; - let comments = match block_on(self.db.send(crate::db::comments::LoadIssueComments { + let comments = match block_on(self.db.send(database_actor::comments::LoadIssueComments { issue_id: msg.issue_id, })) { Ok(Ok(comments)) => comments, Ok(Err(e)) => { - error!("{:?}", e); + log::error!("{:?}", e); return Ok(None); } Err(e) => { - error!("{}", e); + log::error!("{}", e); return Ok(None); } }; @@ -32,7 +32,7 @@ impl WsHandler for WebSocketActor { impl WsHandler for WebSocketActor { fn handle_msg(&mut self, mut msg: CreateCommentPayload, ctx: &mut Self::Context) -> WsResult { - use crate::db::comments::CreateComment; + use database_actor::comments::CreateComment; let user_id = self.require_user()?.id; if msg.user_id.is_none() { @@ -46,11 +46,11 @@ impl WsHandler for WebSocketActor { })) { Ok(Ok(_)) => (), Ok(Err(e)) => { - error!("{:?}", e); + log::error!("{:?}", e); return Ok(None); } Err(e) => { - error!("{}", e); + log::error!("{}", e); return Ok(None); } }; @@ -60,7 +60,7 @@ impl WsHandler for WebSocketActor { impl WsHandler for WebSocketActor { fn handle_msg(&mut self, msg: UpdateCommentPayload, _ctx: &mut Self::Context) -> WsResult { - use crate::db::comments::UpdateComment; + use database_actor::comments::UpdateComment; let user_id = self.require_user()?.id; @@ -76,11 +76,11 @@ impl WsHandler for WebSocketActor { })) { Ok(Ok(comment)) => comment, Ok(Err(e)) => { - error!("{:?}", e); + log::error!("{:?}", e); return Ok(None); } Err(e) => { - error!("{}", e); + log::error!("{}", e); return Ok(None); } }; @@ -95,7 +95,7 @@ pub struct DeleteComment { impl WsHandler for WebSocketActor { fn handle_msg(&mut self, msg: DeleteComment, _ctx: &mut Self::Context) -> WsResult { - use crate::db::comments::DeleteComment; + use database_actor::comments::DeleteComment; let user_id = self.require_user()?.id; @@ -104,17 +104,15 @@ impl WsHandler for WebSocketActor { user_id, }; match block_on(self.db.send(m)) { - Ok(Ok(_)) => (), + Ok(Ok(n)) => Ok(Some(WsMsg::CommentDeleted(msg.comment_id, n))), Ok(Err(e)) => { - error!("{:?}", e); - return Ok(None); + log::error!("{:?}", e); + Ok(None) } Err(e) => { - error!("{}", e); - return Ok(None); + log::error!("{}", e); + Ok(None) } - }; - - Ok(Some(WsMsg::CommentDeleted(msg.comment_id))) + } } } diff --git a/jirs-server/src/ws/epics.rs b/actors/websocket-actor/src/handlers/epics.rs similarity index 79% rename from jirs-server/src/ws/epics.rs rename to actors/websocket-actor/src/handlers/epics.rs index 9da34253..8544b84d 100644 --- a/jirs-server/src/ws/epics.rs +++ b/actors/websocket-actor/src/handlers/epics.rs @@ -2,14 +2,15 @@ use futures::executor::block_on; use jirs_data::{EpicId, NameString, UserProject, WsMsg}; -use crate::ws::{WebSocketActor, WsHandler, WsResult}; +use crate::{WebSocketActor, WsHandler, WsResult}; pub struct LoadEpics; impl WsHandler for WebSocketActor { fn handle_msg(&mut self, _msg: LoadEpics, _ctx: &mut Self::Context) -> WsResult { let project_id = self.require_user_project()?.project_id; - let epics = query_db_or_print!(self, crate::db::epics::LoadEpics { project_id }); + let epics = + crate::query_db_or_print!(self, database_actor::epics::LoadEpics { project_id }); Ok(Some(WsMsg::EpicsLoaded(epics))) } } @@ -26,9 +27,9 @@ impl WsHandler for WebSocketActor { project_id, .. } = self.require_user_project()?; - let epic = query_db_or_print!( + let epic = crate::query_db_or_print!( self, - crate::db::epics::CreateEpic { + database_actor::epics::CreateEpic { user_id: *user_id, project_id: *project_id, name, @@ -47,9 +48,9 @@ impl WsHandler for WebSocketActor { fn handle_msg(&mut self, msg: UpdateEpic, _ctx: &mut Self::Context) -> WsResult { let UpdateEpic { epic_id, name } = msg; let UserProject { project_id, .. } = self.require_user_project()?; - let epic = query_db_or_print!( + let epic = crate::query_db_or_print!( self, - crate::db::epics::UpdateEpic { + database_actor::epics::UpdateEpic { project_id: *project_id, epic_id: epic_id, name: name.clone(), @@ -67,13 +68,13 @@ impl WsHandler for WebSocketActor { fn handle_msg(&mut self, msg: DeleteEpic, _ctx: &mut Self::Context) -> WsResult { let DeleteEpic { epic_id } = msg; let UserProject { user_id, .. } = self.require_user_project()?; - query_db_or_print!( + let n = crate::query_db_or_print!( self, - crate::db::epics::DeleteEpic { + database_actor::epics::DeleteEpic { user_id: *user_id, epic_id: epic_id, } ); - Ok(Some(WsMsg::EpicDeleted(epic_id))) + Ok(Some(WsMsg::EpicDeleted(epic_id, n))) } } diff --git a/jirs-server/src/ws/invitations.rs b/actors/websocket-actor/src/handlers/invitations.rs similarity index 74% rename from jirs-server/src/ws/invitations.rs rename to actors/websocket-actor/src/handlers/invitations.rs index 1a3ad127..16ce4e49 100644 --- a/jirs-server/src/ws/invitations.rs +++ b/actors/websocket-actor/src/handlers/invitations.rs @@ -1,13 +1,12 @@ -use futures::executor::block_on; - -use jirs_data::{ - EmailString, InvitationId, InvitationToken, MessageType, UserRole, UsernameString, WsMsg, +use { + crate::{server::InnerMsg, WebSocketActor, WsHandler, WsMessageSender, WsResult}, + database_actor::{invitations, messages::CreateMessageReceiver}, + futures::executor::block_on, + jirs_data::{ + EmailString, InvitationId, InvitationToken, MessageType, UserRole, UsernameString, WsMsg, + }, }; -use crate::db::invitations; -use crate::db::messages::CreateMessageReceiver; -use crate::ws::{InnerMsg, WebSocketActor, WsHandler, WsMessageSender, WsResult}; - pub struct ListInvitation; impl WsHandler for WebSocketActor { @@ -19,11 +18,11 @@ impl WsHandler for WebSocketActor { let res = match block_on(self.db.send(invitations::ListInvitation { user_id })) { Ok(Ok(v)) => Some(WsMsg::InvitationListLoaded(v)), Ok(Err(e)) => { - error!("{:?}", e); + log::error!("{:?}", e); return Ok(None); } Err(e) => { - error!("{}", e); + log::error!("{}", e); return Ok(None); } }; @@ -46,24 +45,25 @@ impl WsHandler for WebSocketActor { let (user_id, inviter_name) = self.require_user().map(|u| (u.id, u.name.clone()))?; let CreateInvitation { email, name, role } = msg; - let invitation = match block_on(self.db.send(crate::db::invitations::CreateInvitation { - user_id, - project_id, - email: email.clone(), - name: name.clone(), - role, - })) { - Ok(Ok(invitation)) => invitation, - Ok(Err(e)) => { - error!("{:?}", e); - return Ok(Some(WsMsg::InvitationSendFailure)); - } - Err(e) => { - error!("{}", e); - return Ok(Some(WsMsg::InvitationSendFailure)); - } - }; - match block_on(self.mail.send(crate::mail::invite::Invite { + let invitation = + match block_on(self.db.send(database_actor::invitations::CreateInvitation { + user_id, + project_id, + email: email.clone(), + name: name.clone(), + role, + })) { + Ok(Ok(invitation)) => invitation, + Ok(Err(e)) => { + error!("{:?}", e); + return Ok(Some(WsMsg::InvitationSendFailure)); + } + Err(e) => { + error!("{}", e); + return Ok(Some(WsMsg::InvitationSendFailure)); + } + }; + match block_on(self.mail.send(mail_actor::invite::Invite { bind_token: invitation.bind_token, email: invitation.email, inviter_name, @@ -80,7 +80,7 @@ impl WsHandler for WebSocketActor { } // If user exists then send message to him - if let Ok(Ok(message)) = block_on(self.db.send(crate::db::messages::CreateMessage { + if let Ok(Ok(message)) = block_on(self.db.send(database_actor::messages::CreateMessage { receiver: CreateMessageReceiver::Lookup { name, email }, sender_id: user_id, summary: "You have been invited to project".to_string(), @@ -166,19 +166,22 @@ impl WsHandler for WebSocketActor { } }; - for message in block_on(self.db.send(crate::db::messages::LookupMessagesByToken { - token: invitation_token, - user_id: token.user_id, - })) - .unwrap_or_else(|_| Ok(vec![])) - .unwrap_or_default() + for message in block_on( + self.db + .send(database_actor::messages::LookupMessagesByToken { + token: invitation_token, + user_id: token.user_id, + }), + ) + .unwrap_or_else(|_| Ok(vec![])) + .unwrap_or_default() { - match block_on(self.db.send(crate::db::messages::MarkMessageSeen { + match block_on(self.db.send(database_actor::messages::MarkMessageSeen { user_id: token.user_id, message_id: message.id, })) { - Ok(Ok(id)) => { - ctx.send_msg(&WsMsg::MessageMarkedSeen(id)); + Ok(Ok(n)) => { + ctx.send_msg(&WsMsg::MessageMarkedSeen(message.id, n)); } Ok(Err(e)) => { error!("{:?}", e); diff --git a/jirs-server/src/ws/issue_statuses.rs b/actors/websocket-actor/src/handlers/issue_statuses.rs similarity index 95% rename from jirs-server/src/ws/issue_statuses.rs rename to actors/websocket-actor/src/handlers/issue_statuses.rs index b8407fcc..afdce6bd 100644 --- a/jirs-server/src/ws/issue_statuses.rs +++ b/actors/websocket-actor/src/handlers/issue_statuses.rs @@ -1,9 +1,9 @@ use futures::executor::block_on; +use database_actor::issue_statuses; use jirs_data::{IssueStatusId, Position, TitleString, WsMsg}; -use crate::db::issue_statuses; -use crate::ws::{WebSocketActor, WsHandler, WsResult}; +use crate::{WebSocketActor, WsHandler, WsResult}; pub struct LoadIssueStatuses; @@ -71,7 +71,7 @@ impl WsHandler for WebSocketActor { issue_status_id, project_id, })) { - Ok(Ok(is)) => Some(WsMsg::IssueStatusDeleted(is)), + Ok(Ok(n)) => Some(WsMsg::IssueStatusDeleted(msg.issue_status_id, n)), Ok(Err(e)) => { error!("{:?}", e); return Ok(None); diff --git a/jirs-server/src/ws/issues.rs b/actors/websocket-actor/src/handlers/issues.rs similarity index 84% rename from jirs-server/src/ws/issues.rs rename to actors/websocket-actor/src/handlers/issues.rs index 8c947ea4..9678840d 100644 --- a/jirs-server/src/ws/issues.rs +++ b/actors/websocket-actor/src/handlers/issues.rs @@ -1,12 +1,13 @@ -use std::collections::HashMap; - -use futures::executor::block_on; - -use jirs_data::{CreateIssuePayload, IssueAssignee, IssueFieldId, IssueId, PayloadVariant, WsMsg}; - -use crate::db::issue_assignees::LoadAssignees; -use crate::db::issues::{LoadProjectIssues, UpdateIssue}; -use crate::ws::{WebSocketActor, WsHandler, WsResult}; +use { + crate::{WebSocketActor, WsHandler, WsResult}, + database_actor::{ + issue_assignees::LoadAssignees, + issues::{LoadProjectIssues, UpdateIssue}, + }, + futures::executor::block_on, + jirs_data::{CreateIssuePayload, IssueAssignee, IssueFieldId, IssueId, PayloadVariant, WsMsg}, + std::collections::HashMap, +}; pub struct UpdateIssueHandler { pub id: i32, @@ -24,8 +25,23 @@ impl WsHandler for WebSocketActor { payload, } = msg; - let mut msg = UpdateIssue::default(); - msg.issue_id = id; + let mut msg = UpdateIssue { + issue_id: id, + title: None, + issue_type: None, + priority: None, + list_position: None, + description: None, + description_text: None, + estimate: None, + time_spent: None, + time_remaining: None, + project_id: None, + user_ids: None, + reporter_id: None, + issue_status_id: None, + epic_id: None, + }; match (field_id, payload) { (IssueFieldId::Type, PayloadVariant::IssueType(t)) => { msg.issue_type = Some(t); @@ -96,7 +112,7 @@ impl WsHandler for WebSocketActor { impl WsHandler for WebSocketActor { fn handle_msg(&mut self, msg: CreateIssuePayload, _ctx: &mut Self::Context) -> WsResult { self.require_user()?; - let msg = crate::db::issues::CreateIssue { + let msg = database_actor::issues::CreateIssue { title: msg.title, issue_type: msg.issue_type, issue_status_id: msg.issue_status_id, @@ -135,9 +151,9 @@ impl WsHandler for WebSocketActor { self.require_user()?; let m = match block_on( self.db - .send(crate::db::issues::DeleteIssue { issue_id: msg.id }), + .send(database_actor::issues::DeleteIssue { issue_id: msg.id }), ) { - Ok(Ok(_)) => Some(WsMsg::IssueDeleted(msg.id)), + Ok(Ok(n)) => Some(WsMsg::IssueDeleted(msg.id, n)), Ok(Err(e)) => { error!("{:?}", e); return Ok(None); diff --git a/jirs-server/src/ws/messages.rs b/actors/websocket-actor/src/handlers/messages.rs similarity index 88% rename from jirs-server/src/ws/messages.rs rename to actors/websocket-actor/src/handlers/messages.rs index 355665f6..e3b8ddaf 100644 --- a/jirs-server/src/ws/messages.rs +++ b/actors/websocket-actor/src/handlers/messages.rs @@ -1,9 +1,9 @@ use futures::executor::block_on; +use database_actor::messages; use jirs_data::{MessageId, WsMsg}; -use crate::db::messages; -use crate::ws::{WebSocketActor, WsHandler, WsResult}; +use crate::{WebSocketActor, WsHandler, WsResult}; pub struct LoadMessages; @@ -35,7 +35,7 @@ impl WsHandler for WebSocketActor { message_id: msg.id, user_id, })) { - Ok(Ok(id)) => Ok(Some(WsMsg::MessageMarkedSeen(id))), + Ok(Ok(count)) => Ok(Some(WsMsg::MessageMarkedSeen(msg.id, count))), Ok(Err(e)) => { error!("{:?}", e); Ok(None) diff --git a/actors/websocket-actor/src/handlers/mod.rs b/actors/websocket-actor/src/handlers/mod.rs new file mode 100644 index 00000000..fc74ec9e --- /dev/null +++ b/actors/websocket-actor/src/handlers/mod.rs @@ -0,0 +1,15 @@ +pub use { + auth::*, comments::*, epics::*, invitations::*, issue_statuses::*, issues::*, messages::*, + projects::*, user_projects::*, users::*, +}; + +pub mod auth; +pub mod comments; +pub mod epics; +pub mod invitations; +pub mod issue_statuses; +pub mod issues; +pub mod messages; +pub mod projects; +pub mod user_projects; +pub mod users; diff --git a/jirs-server/src/ws/projects.rs b/actors/websocket-actor/src/handlers/projects.rs similarity index 88% rename from jirs-server/src/ws/projects.rs rename to actors/websocket-actor/src/handlers/projects.rs index 4a38b259..5a6fddb6 100644 --- a/jirs-server/src/ws/projects.rs +++ b/actors/websocket-actor/src/handlers/projects.rs @@ -1,9 +1,9 @@ use futures::executor::block_on; +use database_actor as db; use jirs_data::{UpdateProjectPayload, UserProject, WsMsg}; -use crate::db; -use crate::ws::{WebSocketActor, WsHandler, WsResult}; +use crate::{WebSocketActor, WsHandler, WsResult}; impl WsHandler for WebSocketActor { fn handle_msg(&mut self, msg: UpdateProjectPayload, _ctx: &mut Self::Context) -> WsResult { @@ -12,7 +12,7 @@ impl WsHandler for WebSocketActor { project_id, .. } = self.require_user_project()?; - match block_on(self.db.send(crate::db::projects::UpdateProject { + match block_on(self.db.send(database_actor::projects::UpdateProject { project_id: *project_id, name: msg.name, url: msg.url, @@ -32,7 +32,7 @@ impl WsHandler for WebSocketActor { }; let projects = match block_on( self.db - .send(crate::db::projects::LoadProjects { user_id: *user_id }), + .send(database_actor::projects::LoadProjects { user_id: *user_id }), ) { Ok(Ok(projects)) => projects, Ok(Err(e)) => { diff --git a/jirs-server/src/ws/user_projects.rs b/actors/websocket-actor/src/handlers/user_projects.rs similarity index 95% rename from jirs-server/src/ws/user_projects.rs rename to actors/websocket-actor/src/handlers/user_projects.rs index 18364a29..c0e10c81 100644 --- a/jirs-server/src/ws/user_projects.rs +++ b/actors/websocket-actor/src/handlers/user_projects.rs @@ -1,9 +1,9 @@ use futures::executor::block_on; +use database_actor as db; use jirs_data::{UserProjectId, WsMsg}; -use crate::db; -use crate::ws::{WebSocketActor, WsHandler, WsResult}; +use crate::{WebSocketActor, WsHandler, WsResult}; pub struct LoadUserProjects; diff --git a/jirs-server/src/ws/users.rs b/actors/websocket-actor/src/handlers/users.rs similarity index 83% rename from jirs-server/src/ws/users.rs rename to actors/websocket-actor/src/handlers/users.rs index ee83d2ec..d07b7a21 100644 --- a/jirs-server/src/ws/users.rs +++ b/actors/websocket-actor/src/handlers/users.rs @@ -1,17 +1,16 @@ use futures::executor::block_on; use jirs_data::{UserId, UserProject, UserRole, WsMsg}; - -use crate::{ - db::{self, users::Register as DbRegister}, - ws::{auth::Authenticate, WebSocketActor, WsHandler, WsResult}, +use { + crate::{handlers::auth::Authenticate, WebSocketActor, WsHandler, WsResult}, + database_actor::{self, users::Register as DbRegister}, }; pub struct LoadProjectUsers; impl WsHandler for WebSocketActor { fn handle_msg(&mut self, _msg: LoadProjectUsers, _ctx: &mut Self::Context) -> WsResult { - use crate::db::users::LoadProjectUsers as Msg; + use database_actor::users::LoadProjectUsers as Msg; let project_id = self.require_user_project()?.project_id; let m = match block_on(self.db.send(Msg { project_id })) { @@ -66,7 +65,10 @@ impl WsHandler for WebSocketActor { fn handle_msg(&mut self, _msg: LoadInvitedUsers, _ctx: &mut Self::Context) -> WsResult { let user_id = self.require_user()?.id; - let users = match block_on(self.db.send(crate::db::users::LoadInvitedUsers { user_id })) { + let users = match block_on( + self.db + .send(database_actor::users::LoadInvitedUsers { user_id }), + ) { Ok(Ok(users)) => users, _ => return Ok(None), }; @@ -85,7 +87,7 @@ impl WsHandler for WebSocketActor { let user_id = self.require_user()?.id; let ProfileUpdate { name, email } = msg; - match block_on(self.db.send(crate::db::users::ProfileUpdate { + match block_on(self.db.send(database_actor::users::ProfileUpdate { user_id, name, email, @@ -119,11 +121,14 @@ impl WsHandler for WebSocketActor { project_id, .. } = self.require_user_project()?.clone(); - match block_on(self.db.send(db::user_projects::RemoveInvitedUser { - invited_id, - inviter_id, - project_id, - })) { + match block_on( + self.db + .send(database_actor::user_projects::RemoveInvitedUser { + invited_id, + inviter_id, + project_id, + }), + ) { Ok(Ok(_users)) => Ok(Some(WsMsg::InvitedUserRemoveSuccess(invited_id))), Ok(Err(e)) => { error!("{:?}", e); diff --git a/jirs-server/src/ws/mod.rs b/actors/websocket-actor/src/lib.rs similarity index 67% rename from jirs-server/src/ws/mod.rs rename to actors/websocket-actor/src/lib.rs index 97acd05d..7192795f 100644 --- a/jirs-server/src/ws/mod.rs +++ b/actors/websocket-actor/src/lib.rs @@ -1,58 +1,28 @@ -use std::collections::HashMap; +#[macro_use] +extern crate log; -use actix::{ - Actor, ActorContext, Addr, AsyncContext, Context, Handler, Message, Recipient, StreamHandler, -}; -use actix_web::{ - get, - web::{self, Data}, - Error, HttpRequest, HttpResponse, -}; -use actix_web_actors::ws; -use futures::executor::block_on; - -use jirs_data::{Project, ProjectId, User, UserId, UserProject, WsMsg}; - -use crate::db::{projects::LoadCurrentProject, user_projects::CurrentUserProject, DbExecutor}; -use crate::mail::MailExecutor; -use crate::ws::{ - auth::*, - comments::*, - invitations::*, - issue_statuses::*, - issues::*, - messages::*, - projects::*, - user_projects::{LoadUserProjects, SetCurrentUserProject}, - users::*, +use { + crate::{ + handlers::*, + server::{InnerMsg, WsServer}, + }, + actix::{Actor, ActorContext, Addr, AsyncContext, Handler, Recipient, StreamHandler}, + actix_web::{ + get, + web::{self, Data}, + Error, HttpRequest, HttpResponse, + }, + actix_web_actors::ws, + database_actor::{projects::LoadCurrentProject, user_projects::CurrentUserProject, DbExecutor}, + futures::executor::block_on, + jirs_data::{Project, User, UserProject, WsMsg}, + log::*, + mail_actor::MailExecutor, }; -macro_rules! query_db_or_print { - ($s:expr,$msg:expr) => { - match block_on($s.db.send($msg)) { - Ok(Ok(r)) => r, - Ok(Err(e)) => { - error!("{:?}", e); - return Ok(None); - } - Err(e) => { - error!("{}", e); - return Ok(None); - } - } - }; -} - -pub mod auth; -pub mod comments; -pub mod epics; -pub mod invitations; -pub mod issue_statuses; -pub mod issues; -pub mod messages; -pub mod projects; -pub mod user_projects; -pub mod users; +pub mod handlers; +pub mod prelude; +pub mod server; pub type WsResult = std::result::Result, WsMsg>; @@ -346,120 +316,6 @@ where fn handle_msg(&mut self, msg: Message, _ctx: &mut ::Context) -> WsResult; } -#[derive(Message, Debug)] -#[rtype(result = "()")] -pub enum InnerMsg { - Join(ProjectId, UserId, Recipient), - Leave(ProjectId, UserId, Recipient), - BroadcastToChannel(ProjectId, WsMsg), - SendToUser(UserId, WsMsg), - Transfer(WsMsg), -} - -pub struct WsServer { - sessions: HashMap>>, - rooms: HashMap>, -} - -impl Default for WsServer { - fn default() -> Self { - Self { - sessions: HashMap::new(), - rooms: HashMap::new(), - } - } -} - -impl Message for WsServer { - type Result = (); -} - -impl Actor for WsServer { - type Context = Context; -} - -impl Handler for WsServer { - type Result = (); - - fn handle(&mut self, msg: InnerMsg, _ctx: &mut ::Context) -> Self::Result { - debug!("receive {:?}", msg); - match msg { - InnerMsg::Join(project_id, user_id, recipient) => { - let v = self - .sessions - .entry(user_id) - .or_insert_with(Default::default); - v.push(recipient); - self.ensure_room(project_id); - - if let Some(room) = self.rooms.get_mut(&project_id) { - let n = *room.entry(user_id).or_insert(0); - room.insert(user_id, n + 1); - } - } - InnerMsg::Leave(project_id, user_id, recipient) => { - self.ensure_room(project_id); - let room = match self.rooms.get_mut(&project_id) { - Some(room) => room, - None => return, - }; - let n = *room.entry(user_id).or_insert(0); - if n <= 1 { - room.remove(&user_id); - self.sessions.remove(&user_id); - } else { - let v = self.sessions.entry(user_id).or_insert_with(Vec::new); - let mut old = vec![]; - std::mem::swap(&mut old, v); - for r in old { - if r != recipient { - v.push(r); - } - } - } - } - InnerMsg::SendToUser(user_id, msg) => { - if let Some(v) = self.sessions.get(&user_id) { - self.send_to_recipients(v, &msg); - } - } - InnerMsg::BroadcastToChannel(project_id, msg) => { - debug!("Begin broadcast to channel {} msg {:?}", project_id, msg); - let set = match self.rooms.get(&project_id) { - Some(s) => s, - _ => return debug!(" channel not found, aborting..."), - }; - for r in set.keys() { - let v = match self.sessions.get(r) { - Some(v) => v, - _ => { - debug!("recipient is dead, skipping..."); - continue; - } - }; - self.send_to_recipients(v, &msg); - } - } - _ => (), - } - } -} - -impl WsServer { - pub fn ensure_room(&mut self, room: i32) { - self.rooms.entry(room).or_insert_with(HashMap::new); - } - - fn send_to_recipients(&self, recipients: &[Recipient], msg: &WsMsg) { - for recipient in recipients.iter() { - match recipient.do_send(InnerMsg::Transfer(msg.clone())) { - Ok(_) => debug!("msg sent"), - Err(e) => error!("{}", e), - }; - } - } -} - #[get("/ws/")] pub async fn index( req: HttpRequest, diff --git a/actors/websocket-actor/src/prelude.rs b/actors/websocket-actor/src/prelude.rs new file mode 100644 index 00000000..21671326 --- /dev/null +++ b/actors/websocket-actor/src/prelude.rs @@ -0,0 +1,16 @@ +#[macro_export] +macro_rules! query_db_or_print { + ($s:expr,$msg:expr) => { + match block_on($s.db.send($msg)) { + Ok(Ok(r)) => r, + Ok(Err(e)) => { + log::error!("{:?}", e); + return Ok(None); + } + Err(e) => { + log::error!("{}", e); + return Ok(None); + } + } + }; +} diff --git a/actors/websocket-actor/src/server/mod.rs b/actors/websocket-actor/src/server/mod.rs new file mode 100644 index 00000000..9f3c7dc9 --- /dev/null +++ b/actors/websocket-actor/src/server/mod.rs @@ -0,0 +1,119 @@ +use std::collections::HashMap; + +use actix::{Actor, Context, Recipient}; + +use jirs_data::{ProjectId, UserId, WsMsg}; + +#[derive(actix::Message, Debug)] +#[rtype(result = "()")] +pub enum InnerMsg { + Join(ProjectId, UserId, Recipient), + Leave(ProjectId, UserId, Recipient), + BroadcastToChannel(ProjectId, WsMsg), + SendToUser(UserId, WsMsg), + Transfer(WsMsg), +} + +pub struct WsServer { + sessions: HashMap>>, + rooms: HashMap>, +} + +impl Default for WsServer { + fn default() -> Self { + Self { + sessions: HashMap::new(), + rooms: HashMap::new(), + } + } +} + +impl actix::Message for WsServer { + type Result = (); +} + +impl actix::Actor for WsServer { + type Context = Context; +} + +impl actix::Handler for WsServer { + type Result = (); + + fn handle(&mut self, msg: InnerMsg, _ctx: &mut ::Context) -> Self::Result { + debug!("receive {:?}", msg); + match msg { + InnerMsg::Join(project_id, user_id, recipient) => { + let v = self + .sessions + .entry(user_id) + .or_insert_with(Default::default); + v.push(recipient); + self.ensure_room(project_id); + + if let Some(room) = self.rooms.get_mut(&project_id) { + let n = *room.entry(user_id).or_insert(0); + room.insert(user_id, n + 1); + } + } + InnerMsg::Leave(project_id, user_id, recipient) => { + self.ensure_room(project_id); + let room = match self.rooms.get_mut(&project_id) { + Some(room) => room, + None => return, + }; + let n = *room.entry(user_id).or_insert(0); + if n <= 1 { + room.remove(&user_id); + self.sessions.remove(&user_id); + } else { + let v = self.sessions.entry(user_id).or_insert_with(Vec::new); + let mut old = vec![]; + std::mem::swap(&mut old, v); + for r in old { + if r != recipient { + v.push(r); + } + } + } + } + InnerMsg::SendToUser(user_id, msg) => { + if let Some(v) = self.sessions.get(&user_id) { + self.send_to_recipients(v, &msg); + } + } + InnerMsg::BroadcastToChannel(project_id, msg) => { + debug!("Begin broadcast to channel {} msg {:?}", project_id, msg); + let set = match self.rooms.get(&project_id) { + Some(s) => s, + _ => return debug!(" channel not found, aborting..."), + }; + for r in set.keys() { + let v = match self.sessions.get(r) { + Some(v) => v, + _ => { + debug!("recipient is dead, skipping..."); + continue; + } + }; + self.send_to_recipients(v, &msg); + } + } + _ => (), + } + } +} + +impl WsServer { + pub fn ensure_room(&mut self, room: i32) { + self.rooms.entry(room).or_insert_with(HashMap::new); + } + + fn send_to_recipients(&self, recipients: &[Recipient], msg: &WsMsg) { + for recipient in recipients.iter() { + match recipient.do_send(InnerMsg::Transfer(msg.clone())) { + Ok(_) => debug!("msg sent"), + Err(e) => error!("{}", e), + }; + } + } +} diff --git a/jirs-server/diesel.toml b/diesel.toml similarity index 68% rename from jirs-server/diesel.toml rename to diesel.toml index db52bf4c..ca5ae937 100644 --- a/jirs-server/diesel.toml +++ b/diesel.toml @@ -2,7 +2,7 @@ # see diesel.rs/guides/configuring-diesel-cli [print_schema] -file = "src/schema.rs" +file = "database-actor/src/schema.rs" import_types = ["diesel::sql_types::*", "jirs_data::sql::*"] with_docs = true -patch_file = "./src/schema.patch" +patch_file = "./database-actor/src/schema.patch" diff --git a/jirs-client/Cargo.toml b/jirs-client/Cargo.toml index bf26dfd5..d22e1feb 100644 --- a/jirs-client/Cargo.toml +++ b/jirs-client/Cargo.toml @@ -14,7 +14,7 @@ name = "jirs_client" path = "src/lib.rs" [dependencies] -jirs-data = { path = "../jirs-data", features = ["frontend"] } +jirs-data = { path = "../shared/jirs-data", features = ["frontend"] } wee_alloc = "*" diff --git a/jirs-client/src/lib.rs b/jirs-client/src/lib.rs index ab5dad51..5357d775 100644 --- a/jirs-client/src/lib.rs +++ b/jirs-client/src/lib.rs @@ -292,7 +292,7 @@ fn after_mount(url: Url, orders: &mut impl Orders) -> AfterMount { HOST_URL = "".to_string(); WS_URL = "".to_string(); } - model.page = resolve_page(url).unwrap_or_else(|| Page::Project); + model.page = resolve_page(url).unwrap_or(Page::Project); open_socket(&mut model, orders); AfterMount::new(model).url_handling(UrlHandling::PassToRoutes) } diff --git a/jirs-client/src/modal/delete_issue_status.rs b/jirs-client/src/modal/delete_issue_status.rs index a3306b4b..19d4716b 100644 --- a/jirs-client/src/modal/delete_issue_status.rs +++ b/jirs-client/src/modal/delete_issue_status.rs @@ -25,7 +25,10 @@ pub fn update(msg: &Msg, model: &mut Model, orders: &mut impl Orders) { orders, ); } - Msg::WebSocketChange(WebSocketChanged::WsMsg(WsMsg::IssueStatusDeleted(_))) => { + Msg::WebSocketChange(WebSocketChanged::WsMsg(WsMsg::IssueStatusDeleted( + _id, + _n_deleted, + ))) => { orders.skip().send_msg(Msg::ModalDropped); } _ => (), diff --git a/jirs-client/src/project/update.rs b/jirs-client/src/project/update.rs index 3300e8c1..dc615946 100644 --- a/jirs-client/src/project/update.rs +++ b/jirs-client/src/project/update.rs @@ -45,16 +45,18 @@ pub fn update(msg: Msg, model: &mut crate::model::Model, orders: &mut impl Order } } } - Msg::WebSocketChange(WebSocketChanged::WsMsg(WsMsg::IssueDeleted(id))) => { - let mut old: Vec = vec![]; - std::mem::swap(&mut old, &mut model.issues); - for is in old { - if is.id != id { - model.issues.push(is); + Msg::WebSocketChange(WebSocketChanged::WsMsg(WsMsg::IssueDeleted(id, count))) + if count > 0 => + { + let mut old: Vec = vec![]; + std::mem::swap(&mut old, &mut model.issues); + for is in old { + if is.id != id { + model.issues.push(is); + } } + orders.skip().send_msg(Msg::ModalDropped); } - orders.skip().send_msg(Msg::ModalDropped); - } Msg::StyledSelectChanged( FieldId::EditIssueModal(EditIssueModalSection::Issue(IssueFieldId::Type)), StyledSelectChanged::Text(text), diff --git a/jirs-client/src/ws/mod.rs b/jirs-client/src/ws/mod.rs index a369791d..093f54e0 100644 --- a/jirs-client/src/ws/mod.rs +++ b/jirs-client/src/ws/mod.rs @@ -160,10 +160,10 @@ pub fn update(msg: &WsMsg, model: &mut Model, orders: &mut impl Orders) { } } model - .issue_statuses - .sort_by(|a, b| a.position.cmp(&b.position)); + .issue_statuses + .sort_by(|a, b| a.position.cmp(&b.position)); } - WsMsg::IssueStatusDeleted(dropped_id) => { + WsMsg::IssueStatusDeleted(dropped_id, _count) => { let mut old = vec![]; std::mem::swap(&mut model.issue_statuses, &mut old); for is in old { @@ -172,10 +172,10 @@ pub fn update(msg: &WsMsg, model: &mut Model, orders: &mut impl Orders) { } } model - .issue_statuses - .sort_by(|a, b| a.position.cmp(&b.position)); + .issue_statuses + .sort_by(|a, b| a.position.cmp(&b.position)); } - WsMsg::IssueDeleted(id) => { + WsMsg::IssueDeleted(id, _count) => { let mut old = vec![]; std::mem::swap(&mut model.issue_statuses, &mut old); for is in old { @@ -185,7 +185,7 @@ pub fn update(msg: &WsMsg, model: &mut Model, orders: &mut impl Orders) { model.issue_statuses.push(is); } model - .issue_statuses + .issue_statuses .sort_by(|a, b| a.position.cmp(&b.position)); } // users @@ -216,7 +216,7 @@ pub fn update(msg: &WsMsg, model: &mut Model, orders: &mut impl Orders) { } } } - WsMsg::CommentDeleted(comment_id) => { + WsMsg::CommentDeleted(comment_id, _count) => { let mut old = vec![]; std::mem::swap(&mut model.comments, &mut old); for comment in old.into_iter() { @@ -254,7 +254,7 @@ pub fn update(msg: &WsMsg, model: &mut Model, orders: &mut impl Orders) { model.messages = v.clone(); model.messages.sort_by(|a, b| a.id.cmp(&b.id)); } - WsMsg::MessageMarkedSeen(id) => { + WsMsg::MessageMarkedSeen(id, _count) => { let mut old = vec![]; std::mem::swap(&mut old, &mut model.messages); for m in old { @@ -285,7 +285,7 @@ pub fn update(msg: &WsMsg, model: &mut Model, orders: &mut impl Orders) { } model.epics.sort_by(|a, b| a.id.cmp(&b.id)); } - WsMsg::EpicDeleted(id) => { + WsMsg::EpicDeleted(id, _count) => { let mut old = vec![]; std::mem::swap(&mut old, &mut model.epics); for current in old { diff --git a/jirs-server/Cargo.toml b/jirs-server/Cargo.toml index fba97365..cb68ee45 100644 --- a/jirs-server/Cargo.toml +++ b/jirs-server/Cargo.toml @@ -13,29 +13,20 @@ name = "jirs_server" path = "./src/main.rs" [features] -aws-s3 = [ - "rusoto_s3", - "rusoto_core" -] -local-storage = [ - "actix-files" -] +aws-s3 = [] +local-storage = [] default = [ "aws-s3", "local-storage", ] [dependencies] -actix = { version = "*" } +actix = { version = "0.10.0" } actix-web = { version = "*" } actix-cors = { version = "*" } actix-service = { version = "*" } actix-rt = "1" actix-web-actors = "*" -actix-multipart = { version = "*" } - -pq-sys = { version = ">=0.3.0, <0.5.0" } -r2d2 = { version = ">= 0.8, < 0.9" } dotenv = { version = "*" } @@ -67,34 +58,45 @@ futures = { version = "*" } openssl-sys = { version = "*", features = ["vendored"] } libc = { version = "0.2.0", default-features = false } -lettre = { version = "*" } -lettre_email = { version = "*" } - -flate2 = { version = "*" } -syntect = { version = "*" } -lazy_static = { version = "*" } - -[dependencies.diesel] -version = "1.4.5" -features = ["unstable", "postgres", "numeric", "extras", "uuidv07"] +[dependencies.jirs-config] +path = "../shared/jirs-config" +features = ["web", "websocket", "local-storage", "hi", "database"] [dependencies.jirs-data] -path = "../jirs-data" +path = "../shared/jirs-data" features = ["backend"] -# Amazon S3 -[dependencies.rusoto_s3] -optional = true -version = "0.43.0" +[dependencies.highlight-actor] +path = "../actors/highlight-actor" -[dependencies.rusoto_core] -optional = true -version = "0.43.0" +[dependencies.database-actor] +path = "../actors/database-actor" + +[dependencies.web-actor] +path = "../actors/web-actor" + +[dependencies.websocket-actor] +path = "../actors/websocket-actor" + +[dependencies.mail-actor] +path = "../actors/mail-actor" + +[dependencies.filesystem-actor] +path = "../actors/filesystem-actor" + +# Amazon S3 +#[dependencies.rusoto_s3] +#optional = true +#version = "0.43.0" +# +#[dependencies.rusoto_core] +#optional = true +#version = "0.43.0" # Local storage -[dependencies.actix-files] -optional = true -version = "*" +#[dependencies.actix-files] +#optional = true +#version = "*" [dependencies.tokio] version = "0.2.23" diff --git a/jirs-server/src/db/authorize_user.rs b/jirs-server/src/db/authorize_user.rs deleted file mode 100644 index fc130a61..00000000 --- a/jirs-server/src/db/authorize_user.rs +++ /dev/null @@ -1,52 +0,0 @@ -use actix::{Handler, Message}; - -use jirs_data::User; - -use crate::{ - db::{tokens::FindAccessToken, DbExecutor, DbPool, DbPooledConn, SyncQuery}, - db_pool, - errors::ServiceError, -}; - -pub struct AuthorizeUser { - pub access_token: uuid::Uuid, -} - -impl Message for AuthorizeUser { - type Result = Result; -} - -impl AuthorizeUser { - pub fn execute(&self, conn: &DbPooledConn) -> Result { - let token = FindAccessToken { - token: self.access_token, - } - .execute(conn)?; - - crate::db::users::FindUser { - user_id: token.user_id, - } - .execute(conn) - } -} - -impl Handler for DbExecutor { - type Result = Result; - - fn handle(&mut self, msg: AuthorizeUser, _: &mut Self::Context) -> Self::Result { - let conn = db_pool!(self); - msg.execute(conn) - } -} - -impl SyncQuery for AuthorizeUser { - type Result = std::result::Result; - - fn handle(&self, pool: &DbPool) -> Self::Result { - let conn = pool.get().map_err(|e| { - error!("{:?}", e); - crate::errors::ServiceError::DatabaseConnectionLost - })?; - self.execute(&conn) - } -} diff --git a/jirs-server/src/db/comments.rs b/jirs-server/src/db/comments.rs deleted file mode 100644 index bafdde3a..00000000 --- a/jirs-server/src/db/comments.rs +++ /dev/null @@ -1,148 +0,0 @@ -use actix::{Handler, Message}; -use diesel::prelude::*; - -use jirs_data::{msg::WsError, Comment}; - -use crate::{ - db::{DbExecutor, DbPooledConn}, - db_pool, - errors::ServiceError, - q, -}; - -pub struct LoadIssueComments { - pub issue_id: i32, -} - -impl LoadIssueComments { - pub fn execute(self, conn: &DbPooledConn) -> Result, ServiceError> { - use crate::schema::comments::dsl::*; - - q!(comments.distinct_on(id).filter(issue_id.eq(self.issue_id))) - .load(conn) - .map_err(|e| { - error!("{:?}", e); - ServiceError::Error(WsError::FailedToLoadComments) - }) - } -} - -impl Message for LoadIssueComments { - type Result = Result, ServiceError>; -} - -impl Handler for DbExecutor { - type Result = Result, ServiceError>; - - fn handle(&mut self, msg: LoadIssueComments, _ctx: &mut Self::Context) -> Self::Result { - let conn = db_pool!(self); - msg.execute(conn) - } -} - -pub struct CreateComment { - pub user_id: i32, - pub issue_id: i32, - pub body: String, -} - -impl CreateComment { - pub fn execute(self, conn: &DbPooledConn) -> Result { - 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::(conn) - .map_err(|e| { - error!("{:?}", e); - ServiceError::Error(WsError::InvalidComment) - }) - } -} - -impl Message for CreateComment { - type Result = Result; -} - -impl Handler for DbExecutor { - type Result = Result; - - fn handle(&mut self, msg: CreateComment, _ctx: &mut Self::Context) -> Self::Result { - let conn = db_pool!(self); - msg.execute(conn) - } -} - -pub struct UpdateComment { - pub comment_id: i32, - pub user_id: i32, - pub body: String, -} - -impl UpdateComment { - pub fn execute(self, conn: &DbPooledConn) -> Result { - 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::(conn) - .map_err(|e| { - error!("{:?}", e); - ServiceError::Error(WsError::FailedToUpdateComment) - }) - } -} - -impl Message for UpdateComment { - type Result = Result; -} - -impl Handler for DbExecutor { - type Result = Result; - - fn handle(&mut self, msg: UpdateComment, _ctx: &mut Self::Context) -> Self::Result { - let conn = db_pool!(self); - msg.execute(conn) - } -} - -pub struct DeleteComment { - pub comment_id: i32, - pub user_id: i32, -} - -impl DeleteComment { - pub fn execute(self, conn: &DbPooledConn) -> Result { - 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); - ServiceError::Error(WsError::UnableToDeleteComment) - }) - } -} - -impl Message for DeleteComment { - type Result = Result<(), ServiceError>; -} - -impl Handler for DbExecutor { - type Result = Result<(), ServiceError>; - - fn handle(&mut self, msg: DeleteComment, _ctx: &mut Self::Context) -> Self::Result { - let conn = db_pool!(self); - msg.execute(conn)?; - Ok(()) - } -} diff --git a/jirs-server/src/db/epics.rs b/jirs-server/src/db/epics.rs deleted file mode 100644 index cae6cab2..00000000 --- a/jirs-server/src/db/epics.rs +++ /dev/null @@ -1,124 +0,0 @@ -use actix::{Handler, Message}; -use diesel::prelude::*; - -use jirs_data::{msg::WsError, Epic}; - -use crate::{db::DbExecutor, db_pool, errors::ServiceError, q}; - -pub struct LoadEpics { - pub project_id: i32, -} - -impl Message for LoadEpics { - type Result = Result, ServiceError>; -} - -impl Handler for DbExecutor { - type Result = Result, ServiceError>; - - fn handle(&mut self, msg: LoadEpics, _ctx: &mut Self::Context) -> Self::Result { - use crate::schema::epics::dsl::*; - - let conn = db_pool!(self); - - q!(epics.distinct_on(id).filter(project_id.eq(msg.project_id))) - .load(conn) - .map_err(|e| { - error!("{:?}", e); - ServiceError::Error(WsError::FailedToLoadEpics) - }) - } -} - -pub struct CreateEpic { - pub user_id: i32, - pub project_id: i32, - pub name: String, -} - -impl Message for CreateEpic { - type Result = Result; -} - -impl Handler for DbExecutor { - type Result = Result; - - fn handle(&mut self, msg: CreateEpic, _ctx: &mut Self::Context) -> Self::Result { - use crate::schema::epics::dsl::*; - - let conn = db_pool!(self); - - q!(diesel::insert_into(epics).values(( - name.eq(msg.name.as_str()), - user_id.eq(msg.user_id), - project_id.eq(msg.project_id), - ))) - .get_result::(conn) - .map_err(|e| { - error!("{:?}", e); - ServiceError::Error(WsError::InvalidEpic) - }) - } -} - -pub struct UpdateEpic { - pub epic_id: i32, - pub project_id: i32, - pub name: String, -} - -impl Message for UpdateEpic { - type Result = Result; -} - -impl Handler for DbExecutor { - type Result = Result; - - fn handle(&mut self, msg: UpdateEpic, _ctx: &mut Self::Context) -> Self::Result { - use crate::schema::epics::dsl::*; - - 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))) - .get_result::(conn) - .map_err(|e| { - error!("{:?}", e); - ServiceError::Error(WsError::FailedToUpdateEpic) - }) - } -} - -pub struct DeleteEpic { - pub epic_id: i32, - pub user_id: i32, -} - -impl Message for DeleteEpic { - type Result = Result<(), ServiceError>; -} - -impl Handler for DbExecutor { - type Result = Result<(), ServiceError>; - - fn handle(&mut self, msg: DeleteEpic, _ctx: &mut Self::Context) -> Self::Result { - use crate::schema::epics::dsl::*; - - let conn = db_pool!(self); - - q!(diesel::delete( - epics.filter(user_id.eq(msg.user_id)).find(msg.epic_id) - )) - .execute(conn) - .map_err(|e| { - error!("{:?}", e); - ServiceError::Error(WsError::UnableToDeleteEpic) - })?; - - Ok(()) - } -} diff --git a/jirs-server/src/db/invitations.rs b/jirs-server/src/db/invitations.rs deleted file mode 100644 index 493b3043..00000000 --- a/jirs-server/src/db/invitations.rs +++ /dev/null @@ -1,300 +0,0 @@ -use actix::{Handler, Message}; -use diesel::prelude::*; - -use jirs_data::{ - 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::ServiceError, - q, -}; - -pub struct FindByBindToken { - pub token: InvitationToken, -} - -impl FindByBindToken { - pub fn execute(self, conn: &DbPooledConn) -> Result { - use crate::schema::invitations::dsl::*; - q!(invitations.filter(bind_token.eq(self.token))) - .first(conn) - .map_err(|e| ServiceError::DatabaseQueryFailed(format!("{}", e))) - } -} - -impl Message for FindByBindToken { - type Result = Result; -} - -impl Handler for DbExecutor { - type Result = Result; - - 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, -} - -impl Message for ListInvitation { - type Result = Result, ServiceError>; -} - -impl Handler for DbExecutor { - type Result = Result, ServiceError>; - - fn handle(&mut self, msg: ListInvitation, _ctx: &mut Self::Context) -> Self::Result { - use crate::schema::invitations::dsl::*; - - let conn = db_pool!(self); - - 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())) - .load(conn) - .map_err(|e| { - error!("{:?}", e); - ServiceError::Error(WsError::FailedToLoadInvitations) - }) - } -} - -pub struct CreateInvitation { - pub user_id: UserId, - pub project_id: ProjectId, - pub email: EmailString, - pub name: UsernameString, - pub role: UserRole, -} - -impl CreateInvitation { - pub fn execute(self, conn: &DbPooledConn) -> Result { - 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); - ServiceError::Error(WsError::InvalidInvitation) - }) - } -} - -impl Message for CreateInvitation { - type Result = Result; -} - -impl Handler for DbExecutor { - type Result = Result; - - fn handle(&mut self, msg: CreateInvitation, _ctx: &mut Self::Context) -> Self::Result { - let conn = db_pool!(self); - msg.execute(conn) - } -} - -pub struct DeleteInvitation { - pub id: InvitationId, -} - -impl DeleteInvitation { - pub fn execute(self, conn: &DbPooledConn) -> Result { - use crate::schema::invitations::dsl::*; - q!(diesel::delete(invitations).filter(id.eq(self.id))) - .execute(conn) - .map_err(|e| { - error!("{:?}", e); - ServiceError::Error(WsError::UnableToDeleteInvitation) - }) - } -} - -impl Message for DeleteInvitation { - type Result = Result<(), ServiceError>; -} - -impl Handler for DbExecutor { - type Result = Result<(), ServiceError>; - - fn handle(&mut self, msg: DeleteInvitation, _ctx: &mut Self::Context) -> Self::Result { - let conn = db_pool!(self); - msg.execute(conn)?; - Ok(()) - } -} - -struct UpdateInvitationState { - pub id: InvitationId, - pub state: InvitationState, -} - -impl UpdateInvitationState { - pub fn execute(self, conn: &DbPooledConn) -> Result { - 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); - ServiceError::Error(WsError::FailedToUpdateInvitation) - }) - } -} - -impl Message for UpdateInvitationState { - type Result = Result<(), ServiceError>; -} - -impl Handler for DbExecutor { - type Result = Result<(), ServiceError>; - - fn handle(&mut self, msg: UpdateInvitationState, _ctx: &mut Self::Context) -> Self::Result { - let conn = db_pool!(self); - msg.execute(conn)?; - Ok(()) - } -} - -pub struct RevokeInvitation { - pub id: InvitationId, -} - -impl Message for RevokeInvitation { - type Result = Result<(), ServiceError>; -} - -impl Handler for DbExecutor { - type Result = Result<(), ServiceError>; - - fn handle(&mut self, msg: RevokeInvitation, _ctx: &mut Self::Context) -> Self::Result { - let conn = db_pool!(self); - UpdateInvitationState { - id: msg.id, - state: InvitationState::Revoked, - } - .execute(conn)?; - Ok(()) - } -} - -pub struct AcceptInvitation { - pub invitation_token: InvitationToken, -} - -impl AcceptInvitation { - pub fn execute(self, conn: &DbPooledConn) -> Result { - use crate::schema::invitations::dsl::*; - - crate::db::Guard::new(conn)?.run::(|_guard| { - let invitation = crate::db::invitations::FindByBindToken { - token: self.invitation_token, - } - .execute(conn)?; - - if invitation.state == InvitationState::Revoked { - return Err(ServiceError::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| { - ServiceError::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(ServiceError::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 { - 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; -} - -impl Handler for DbExecutor { - type Result = Result; - - fn handle(&mut self, msg: AcceptInvitation, _ctx: &mut Self::Context) -> Self::Result { - let conn = db_pool!(self); - - msg.execute(conn) - } -} diff --git a/jirs-server/src/db/issue_assignees.rs b/jirs-server/src/db/issue_assignees.rs deleted file mode 100644 index 204191c5..00000000 --- a/jirs-server/src/db/issue_assignees.rs +++ /dev/null @@ -1,44 +0,0 @@ -use actix::{Handler, Message}; -use diesel::prelude::*; - -use jirs_data::IssueAssignee; - -use crate::{ - db::{DbExecutor, DbPooledConn}, - db_pool, - errors::ServiceError, - q, -}; - -pub struct LoadAssignees { - pub issue_id: i32, -} - -impl LoadAssignees { - pub fn execute(self, conn: &DbPooledConn) -> Result, ServiceError> { - use crate::schema::issue_assignees::dsl::*; - - q!(issue_assignees - .distinct_on(id) - .filter(issue_id.eq(self.issue_id))) - .load::(conn) - .map_err(|e| { - error!("{:?}", e); - ServiceError::RecordNotFound("issue users".to_string()) - }) - } -} - -impl Message for LoadAssignees { - type Result = Result, ServiceError>; -} - -impl Handler for DbExecutor { - type Result = Result, ServiceError>; - - fn handle(&mut self, msg: LoadAssignees, _ctx: &mut Self::Context) -> Self::Result { - let conn = db_pool!(self); - - msg.execute(conn) - } -} diff --git a/jirs-server/src/db/issue_statuses.rs b/jirs-server/src/db/issue_statuses.rs deleted file mode 100644 index 632a4d4b..00000000 --- a/jirs-server/src/db/issue_statuses.rs +++ /dev/null @@ -1,142 +0,0 @@ -use actix::{Handler, Message}; -use diesel::prelude::*; - -use jirs_data::{IssueStatus, IssueStatusId, Position, ProjectId, TitleString}; - -use crate::db::DbPooledConn; -use crate::{db::DbExecutor, db_pool, errors::ServiceError, q}; - -pub struct LoadIssueStatuses { - pub project_id: ProjectId, -} - -impl LoadIssueStatuses { - pub fn execute(self, conn: &DbPooledConn) -> Result, ServiceError> { - 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::(conn) - .map_err(|e| { - error!("{:?}", e); - ServiceError::RecordNotFound("issue users".to_string()) - }) - } -} - -impl Message for LoadIssueStatuses { - type Result = Result, ServiceError>; -} - -impl Handler for DbExecutor { - type Result = Result, ServiceError>; - - fn handle(&mut self, msg: LoadIssueStatuses, _ctx: &mut Self::Context) -> Self::Result { - let conn = db_pool!(self); - - msg.execute(conn) - } -} - -pub struct CreateIssueStatus { - pub project_id: ProjectId, - pub position: i32, - pub name: TitleString, -} - -impl CreateIssueStatus { - pub fn execute(self, conn: &DbPooledConn) -> Result { - 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::(conn) - .map_err(|e| { - error!("{:?}", e); - ServiceError::RecordNotFound("issue users".to_string()) - }) - } -} - -impl Message for CreateIssueStatus { - type Result = Result; -} - -impl Handler for DbExecutor { - type Result = Result; - - fn handle(&mut self, msg: CreateIssueStatus, _ctx: &mut Self::Context) -> Self::Result { - let conn = db_pool!(self); - - msg.execute(conn) - } -} - -pub struct DeleteIssueStatus { - pub project_id: ProjectId, - pub issue_status_id: IssueStatusId, -} - -impl Message for DeleteIssueStatus { - type Result = Result; -} - -impl Handler for DbExecutor { - type Result = Result; - - 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 = db_pool!(self); - - q!(diesel::delete(issue_statuses) - .filter(id.eq(msg.issue_status_id)) - .filter(project_id.eq(msg.project_id))) - .execute(conn) - .map_err(|e| { - error!("{:?}", e); - ServiceError::RecordNotFound("issue users".to_string()) - })?; - Ok(msg.issue_status_id) - } -} - -pub struct UpdateIssueStatus { - pub issue_status_id: IssueStatusId, - pub project_id: ProjectId, - pub position: Position, - pub name: TitleString, -} - -impl Message for UpdateIssueStatus { - type Result = Result; -} - -impl Handler for DbExecutor { - type Result = Result; - - fn handle(&mut self, msg: UpdateIssueStatus, _ctx: &mut Self::Context) -> Self::Result { - use crate::schema::issue_statuses::dsl::{ - id, issue_statuses, name, position, project_id, updated_at, - }; - - let conn = db_pool!(self); - - 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))) - .get_result::(conn) - .map_err(|e| { - error!("{:?}", e); - ServiceError::RecordNotFound("issue users".to_string()) - }) - } -} diff --git a/jirs-server/src/db/issues.rs b/jirs-server/src/db/issues.rs deleted file mode 100644 index 49f8d564..00000000 --- a/jirs-server/src/db/issues.rs +++ /dev/null @@ -1,318 +0,0 @@ -use actix::{Handler, Message}; -use diesel::expression::dsl::not; -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, db_pool, errors::ServiceError, models::Issue}; - -const FAILED_CONNECT_USER_AND_ISSUE: &str = "Failed to create connection between user and issue"; - -#[derive(Serialize, Deserialize)] -pub struct LoadIssue { - pub issue_id: i32, -} - -impl Message for LoadIssue { - type Result = Result; -} - -impl Handler for DbExecutor { - type Result = Result; - - fn handle(&mut self, msg: LoadIssue, _ctx: &mut Self::Context) -> Self::Result { - use crate::schema::issues::dsl::{id, issues}; - - let conn = db_pool!(self); - - let query = issues.filter(id.eq(msg.issue_id)).distinct(); - debug!( - "{}", - diesel::debug_query::(&query).to_string() - ); - query.first::(conn).map_err(|e| { - error!("{:?}", e); - ServiceError::RecordNotFound("project issues".to_string()) - }) - } -} - -#[derive(Serialize, Deserialize)] -pub struct LoadProjectIssues { - pub project_id: i32, -} - -impl Message for LoadProjectIssues { - type Result = Result, ServiceError>; -} - -impl Handler for DbExecutor { - type Result = Result, ServiceError>; - - fn handle(&mut self, msg: LoadProjectIssues, _ctx: &mut Self::Context) -> Self::Result { - use crate::schema::issues::dsl::{issues, project_id}; - - let conn = db_pool!(self); - - let chain = issues.filter(project_id.eq(msg.project_id)).distinct(); - debug!( - "{}", - diesel::debug_query::(&chain).to_string() - ); - let vec = chain.load::(conn).map_err(|e| { - error!("{:?}", e); - ServiceError::RecordNotFound("project issues".to_string()) - })?; - Ok(vec) - } -} - -#[derive(Serialize, Deserialize, Default)] -pub struct UpdateIssue { - pub issue_id: i32, - pub title: Option, - pub issue_type: Option, - pub priority: Option, - pub list_position: Option, - pub description: Option, - pub description_text: Option, - pub estimate: Option, - pub time_spent: Option, - pub time_remaining: Option, - pub project_id: Option, - pub user_ids: Option>, - pub reporter_id: Option, - pub issue_status_id: Option, - pub epic_id: Option>, -} - -impl Message for UpdateIssue { - type Result = Result; -} - -impl Handler for DbExecutor { - type Result = Result; - - fn handle(&mut self, msg: UpdateIssue, _ctx: &mut Self::Context) -> Self::Result { - use crate::schema::issues::dsl::{self, issues}; - - let conn = db_pool!(self); - - let current_issue_id = msg.issue_id; - - let chain = diesel::update(issues.find(current_issue_id)).set(( - msg.title.map(|title| dsl::title.eq(title)), - msg.issue_type - .map(|issue_type| dsl::issue_type.eq(issue_type)), - msg.issue_status_id.map(|id| dsl::issue_status_id.eq(id)), - msg.priority.map(|priority| dsl::priority.eq(priority)), - msg.list_position - .map(|list_position| dsl::list_position.eq(list_position)), - msg.description - .map(|description| dsl::description.eq(description)), - msg.description_text - .map(|description_text| dsl::description_text.eq(description_text)), - msg.estimate.map(|estimate| dsl::estimate.eq(estimate)), - msg.time_spent - .map(|time_spent| dsl::time_spent.eq(time_spent)), - msg.time_remaining - .map(|time_remaining| dsl::time_remaining.eq(time_remaining)), - msg.project_id - .map(|project_id| dsl::project_id.eq(project_id)), - msg.reporter_id - .map(|reporter_id| dsl::reporter_id.eq(reporter_id)), - msg.epic_id.map(|epic_id| dsl::epic_id.eq(epic_id)), - dsl::updated_at.eq(chrono::Utc::now().naive_utc()), - )); - debug!( - "{}", - diesel::debug_query::(&chain).to_string() - ); - chain.get_result::(conn).map_err(|e| { - error!("{:?}", e); - ServiceError::DatabaseQueryFailed("Failed to update issue".to_string()) - })?; - - if let Some(user_ids) = msg.user_ids.as_ref() { - use crate::schema::issue_assignees::dsl; - 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(|e| { - error!("{:?}", e); - ServiceError::DatabaseConnectionLost - })?; - let existing: Vec = dsl::issue_assignees - .select(dsl::user_id) - .filter(dsl::issue_id.eq(current_issue_id)) - .get_results::(conn) - .map_err(|e| { - error!("{:?}", e); - ServiceError::DatabaseConnectionLost - })?; - let mut values = vec![]; - for user_id in user_ids.iter() { - if !existing.contains(user_id) { - values.push(crate::models::CreateIssueAssigneeForm { - issue_id: current_issue_id, - user_id: *user_id, - }) - } - } - diesel::insert_into(dsl::issue_assignees) - .values(values) - .execute(conn) - .map_err(|e| { - error!("{:?}", e); - ServiceError::DatabaseQueryFailed(FAILED_CONNECT_USER_AND_ISSUE.to_string()) - })?; - } - - issues.find(msg.issue_id).first::(conn).map_err(|e| { - error!("{:?}", e); - ServiceError::DatabaseConnectionLost - }) - } -} - -#[derive(Serialize, Deserialize)] -pub struct DeleteIssue { - pub issue_id: i32, -} - -impl Message for DeleteIssue { - type Result = Result<(), ServiceError>; -} - -impl Handler for DbExecutor { - type Result = Result<(), ServiceError>; - - fn handle(&mut self, msg: DeleteIssue, _ctx: &mut Self::Context) -> Self::Result { - use crate::schema::issue_assignees::dsl::{issue_assignees, issue_id}; - use crate::schema::issues::dsl::issues; - - let conn = db_pool!(self); - - diesel::delete(issue_assignees.filter(issue_id.eq(msg.issue_id))) - .execute(conn) - .map_err(|e| ServiceError::RecordNotFound(format!("issue {}. {}", msg.issue_id, e)))?; - diesel::delete(issues.find(msg.issue_id)) - .execute(conn) - .map_err(|e| ServiceError::RecordNotFound(format!("issue {}. {}", msg.issue_id, e)))?; - Ok(()) - } -} - -#[derive(Serialize, Deserialize)] -pub struct CreateIssue { - pub title: String, - pub issue_type: IssueType, - pub issue_status_id: IssueStatusId, - pub priority: IssuePriority, - pub description: Option, - pub description_text: Option, - pub estimate: Option, - pub time_spent: Option, - pub time_remaining: Option, - pub project_id: jirs_data::ProjectId, - pub reporter_id: jirs_data::UserId, - pub user_ids: Vec, - pub epic_id: Option, -} - -impl Message for CreateIssue { - type Result = Result; -} - -impl Handler for DbExecutor { - type Result = Result; - - fn handle(&mut self, msg: CreateIssue, ctx: &mut Self::Context) -> Self::Result { - use crate::schema::issue_assignees::dsl; - use crate::schema::issues::dsl::issues; - - let conn = db_pool!(self); - - let list_position = issues - // .filter(issue_status_id.eq(IssueStatus::Backlog)) - .select(sql("COALESCE(max(list_position), 0) + 1")) - .get_result::(conn) - .map_err(|e| { - error!("resolve new issue position failed {}", e); - ServiceError::DatabaseConnectionLost - })?; - - info!("{:?}", msg.issue_type); - info!("msg.issue_status_id {:?}", msg.issue_status_id); - - let issue_status_id = if msg.issue_status_id == 0 { - self.handle( - crate::db::issue_statuses::LoadIssueStatuses { - project_id: msg.project_id, - }, - ctx, - ) - .map_err(|e| { - error!("{:?}", e); - ServiceError::Error(WsError::FailedToFetchIssueStatuses) - })? - .get(0) - .ok_or_else(|| ServiceError::Error(WsError::NoIssueStatuses))? - .id - } else { - msg.issue_status_id - }; - - let form = crate::models::CreateIssueForm { - title: msg.title, - issue_type: msg.issue_type, - issue_status_id, - priority: msg.priority, - list_position, - description: msg.description, - description_text: msg.description_text, - estimate: msg.estimate, - time_spent: msg.time_spent, - time_remaining: msg.time_remaining, - reporter_id: msg.reporter_id, - project_id: msg.project_id, - epic_id: msg.epic_id, - }; - - let issue = diesel::insert_into(issues) - .values(form) - .on_conflict_do_nothing() - .get_result::(conn) - .map_err(|e| { - error!("{}", e); - ServiceError::DatabaseConnectionLost - })?; - - let mut values = vec![]; - for user_id in msg.user_ids.iter() { - values.push(crate::models::CreateIssueAssigneeForm { - issue_id: issue.id, - user_id: *user_id, - }); - } - if !msg.user_ids.contains(&msg.reporter_id) { - values.push(crate::models::CreateIssueAssigneeForm { - issue_id: issue.id, - user_id: msg.reporter_id, - }); - } - - diesel::insert_into(dsl::issue_assignees) - .values(values) - .execute(conn) - .map_err(|e| { - error!("{:?}", e); - ServiceError::DatabaseConnectionLost - })?; - - Ok(issue) - } -} diff --git a/jirs-server/src/db/messages.rs b/jirs-server/src/db/messages.rs deleted file mode 100644 index 18691ad9..00000000 --- a/jirs-server/src/db/messages.rs +++ /dev/null @@ -1,174 +0,0 @@ -use actix::Handler; -use diesel::prelude::*; - -use jirs_data::{BindToken, Message, MessageId, MessageType, User, UserId}; - -use crate::{ - db::{ - users::{FindUser, LookupUser}, - DbExecutor, - }, - db_pool, - errors::ServiceError, - q, -}; - -#[derive(Debug)] -pub struct LoadMessages { - pub user_id: UserId, -} - -impl actix::Message for LoadMessages { - type Result = Result, ServiceError>; -} - -impl Handler for DbExecutor { - type Result = Result, ServiceError>; - - fn handle(&mut self, msg: LoadMessages, _ctx: &mut Self::Context) -> Self::Result { - use crate::schema::messages::dsl::*; - - let conn = db_pool!(self); - - q!(messages.filter(receiver_id.eq(msg.user_id))) - .load(conn) - .map_err(|e| { - error!("{:?}", e); - ServiceError::DatabaseQueryFailed("load user messages".to_string()) - }) - } -} - -#[derive(Debug)] -pub struct MarkMessageSeen { - pub user_id: UserId, - pub message_id: MessageId, -} - -impl actix::Message for MarkMessageSeen { - type Result = Result; -} - -impl Handler for DbExecutor { - type Result = Result; - - fn handle(&mut self, msg: MarkMessageSeen, _ctx: &mut Self::Context) -> Self::Result { - use crate::schema::messages::dsl::*; - - let conn = db_pool!(self); - - let size = q!(diesel::delete( - messages - .find(msg.message_id) - .filter(receiver_id.eq(msg.user_id)), - )) - .execute(conn) - .map_err(|e| { - error!("{:?}", e); - ServiceError::DatabaseQueryFailed("load user messages".to_string()) - })?; - - if size > 0 { - Ok(msg.message_id) - } else { - Err(ServiceError::DatabaseQueryFailed(format!( - "failed to delete message for {:?}", - msg - ))) - } - } -} - -#[derive(Debug)] -pub enum CreateMessageReceiver { - Reference(UserId), - Lookup { name: String, email: String }, -} - -#[derive(Debug)] -pub struct CreateMessage { - pub receiver: CreateMessageReceiver, - pub sender_id: UserId, - pub summary: String, - pub description: String, - pub message_type: MessageType, - pub hyper_link: String, -} - -impl actix::Message for CreateMessage { - type Result = Result; -} - -impl Handler for DbExecutor { - type Result = Result; - - fn handle(&mut self, msg: CreateMessage, ctx: &mut Self::Context) -> Self::Result { - use crate::schema::messages::dsl::*; - - let conn = db_pool!(self); - - let user: User = match { - match msg.receiver { - CreateMessageReceiver::Lookup { name, email } => { - self.handle(LookupUser { name, email }, ctx) - } - CreateMessageReceiver::Reference(user_id) => self.handle(FindUser { user_id }, ctx), - } - } { - Ok(user) => user, - _ => { - return Err(ServiceError::RecordNotFound( - "No matching user found".to_string(), - )); - } - }; - - let query = diesel::insert_into(messages).values(( - receiver_id.eq(user.id), - sender_id.eq(msg.sender_id), - summary.eq(msg.summary), - description.eq(msg.description), - message_type.eq(msg.message_type), - hyper_link.eq(msg.hyper_link), - )); - debug!( - "{}", - diesel::debug_query::(&query).to_string() - ); - query.get_result(conn).map_err(|e| { - error!("{:?}", e); - ServiceError::DatabaseQueryFailed("create message failed".to_string()) - }) - } -} - -#[derive(Debug)] -pub struct LookupMessagesByToken { - pub token: BindToken, - pub user_id: UserId, -} - -impl actix::Message for LookupMessagesByToken { - type Result = Result, ServiceError>; -} - -impl Handler for DbExecutor { - type Result = Result, ServiceError>; - - fn handle(&mut self, msg: LookupMessagesByToken, _ctx: &mut Self::Context) -> Self::Result { - use crate::schema::messages::dsl::*; - - let conn = db_pool!(self); - - q!(messages.filter( - hyper_link - .eq(format!("#{}", msg.token)) - .and(receiver_id.eq(msg.user_id)), - )) - .load(conn) - .map_err(|e| { - error!("{:?}", e); - ServiceError::DatabaseQueryFailed("create message failed".to_string()) - }) - } -} diff --git a/jirs-server/src/db/mod.rs b/jirs-server/src/db/mod.rs deleted file mode 100644 index cd7abb31..00000000 --- a/jirs-server/src/db/mod.rs +++ /dev/null @@ -1,175 +0,0 @@ -use std::fs::*; - -use actix::{Actor, SyncContext}; -use diesel::pg::PgConnection; -use diesel::r2d2::{self, ConnectionManager}; -use serde::{Deserialize, Serialize}; - -use crate::errors::ServiceError; - -pub mod authorize_user; -pub mod comments; -pub mod epics; -pub mod invitations; -pub mod issue_assignees; -pub mod issue_statuses; -pub mod issues; -pub mod messages; -pub mod projects; -pub mod tokens; -pub mod user_projects; -pub mod users; - -pub type DbPool = r2d2::Pool>; -pub type DbPooledConn = r2d2::PooledConnection>; - -pub struct DbExecutor { - pub pool: DbPool, - pub config: Configuration, -} - -impl Actor for DbExecutor { - type Context = SyncContext; -} - -impl Default for DbExecutor { - fn default() -> Self { - Self { - pool: build_pool(), - config: Configuration::read(), - } - } -} - -pub fn build_pool() -> DbPool { - dotenv::dotenv().ok(); - let config = Configuration::read(); - - let manager = ConnectionManager::::new(config.database_url); - r2d2::Pool::builder() - .max_size(config.concurrency as u32) - .build(manager) - .unwrap_or_else(|e| panic!("Failed to create pool. {}", e)) -} - -pub trait SyncQuery { - type Result; - - fn handle(&self, pool: &DbPool) -> Self::Result; -} - -#[derive(Serialize, Deserialize)] -pub struct Configuration { - pub concurrency: usize, - pub database_url: String, -} - -impl Default for Configuration { - fn default() -> Self { - let database_url = if cfg!(test) { - "postgres://postgres@localhost:5432/jirs_test".to_string() - } else { - std::env::var("DATABASE_URL") - .unwrap_or_else(|_| "postgres://postgres@localhost:5432/jirs".to_string()) - }; - Self { - concurrency: 2, - database_url, - } - } -} - -impl Configuration { - pub fn read() -> Self { - let contents: String = read_to_string(Self::config_file()).unwrap_or_default(); - match toml::from_str(contents.as_str()) { - Ok(config) => config, - _ => { - let config = Configuration::default(); - config.write().unwrap_or_else(|e| panic!(e)); - config - } - } - } - - pub fn write(&self) -> Result<(), String> { - let s = toml::to_string(self).map_err(|e| e.to_string())?; - write(Self::config_file(), s.as_str()).map_err(|e| e.to_string())?; - Ok(()) - } - - #[cfg(not(test))] - pub fn config_file() -> &'static str { - "db.toml" - } - - #[cfg(test)] - pub fn config_file() -> &'static str { - "db.test.toml" - } -} - -#[macro_export] -macro_rules! db_pool { - ($self: expr) => { - &$self.pool.get().map_err(|e| { - error!("{:?}", e); - ServiceError::DatabaseConnectionLost - })? - }; -} - -#[macro_export] -macro_rules! q { - ($q: expr) => {{ - let q = $q; - debug!( - "{}", - diesel::debug_query::(&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 { - use diesel::{connection::TransactionManager, prelude::*}; - let tm = conn.transaction_manager(); - tm.begin_transaction(conn).map_err(|e| { - log::error!("{:?}", e); - ServiceError::DatabaseConnectionLost - })?; - Ok(Self { conn, tm }) - } - - pub fn run Result>( - &self, - f: F, - ) -> Result { - use diesel::connection::TransactionManager; - - let r = f(self); - match r { - Ok(r) => { - self.tm.commit_transaction(self.conn).map_err(|e| { - log::error!("{:?}", e); - ServiceError::DatabaseConnectionLost - })?; - Ok(r) - } - Err(e) => { - log::error!("{:?}", e); - self.tm.rollback_transaction(self.conn).map_err(|e| { - log::error!("{:?}", e); - ServiceError::DatabaseConnectionLost - })?; - Err(e) - } - } - } -} diff --git a/jirs-server/src/db/projects.rs b/jirs-server/src/db/projects.rs deleted file mode 100644 index f633738f..00000000 --- a/jirs-server/src/db/projects.rs +++ /dev/null @@ -1,174 +0,0 @@ -use actix::{Handler, Message}; -use diesel::prelude::*; -use serde::{Deserialize, Serialize}; - -use jirs_data::{NameString, Project, ProjectCategory, ProjectId, TimeTracking, UserId}; - -use crate::db::DbPooledConn; -use crate::{db::DbExecutor, db_pool, errors::ServiceError, q, schema::projects::all_columns}; - -#[derive(Serialize, Deserialize)] -pub struct LoadCurrentProject { - pub project_id: ProjectId, -} - -impl LoadCurrentProject { - pub fn execute(self, conn: &DbPooledConn) -> Result { - use crate::schema::projects::dsl::projects; - - q!(projects.find(self.project_id)) - .first::(conn) - .map_err(|e| { - error!("{:?}", e); - ServiceError::RecordNotFound("Project".to_string()) - }) - } -} - -impl Message for LoadCurrentProject { - type Result = Result; -} - -impl Handler for DbExecutor { - type Result = Result; - - fn handle(&mut self, msg: LoadCurrentProject, _ctx: &mut Self::Context) -> Self::Result { - let conn = db_pool!(self); - - msg.execute(conn) - } -} - -pub struct CreateProject { - pub name: NameString, - pub url: Option, - pub description: Option, - pub category: Option, - pub time_tracking: Option, -} - -impl CreateProject { - pub fn execute(self, conn: &DbPooledConn) -> Result { - 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::(conn) - .map_err(|e| { - error!("{:?}", e); - ServiceError::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; -} - -impl Handler for DbExecutor { - type Result = Result; - - fn handle(&mut self, msg: CreateProject, _ctx: &mut Self::Context) -> Self::Result { - let conn = db_pool!(self); - - msg.execute(conn) - } -} - -pub struct UpdateProject { - pub project_id: ProjectId, - pub name: Option, - pub url: Option, - pub description: Option, - pub category: Option, - pub time_tracking: Option, -} - -impl UpdateProject { - pub fn execute(self, conn: &DbPooledConn) -> Result { - 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| ServiceError::DatabaseQueryFailed(format!("{}", e)))?; - - LoadCurrentProject { - project_id: self.project_id, - } - .execute(conn) - } -} - -impl Message for UpdateProject { - type Result = Result; -} - -impl Handler for DbExecutor { - type Result = Result; - - fn handle(&mut self, msg: UpdateProject, _ctx: &mut Self::Context) -> Self::Result { - let conn = db_pool!(self); - - msg.execute(conn) - } -} - -pub struct LoadProjects { - pub user_id: UserId, -} - -impl LoadProjects { - pub fn execute(self, conn: &DbPooledConn) -> Result, ServiceError> { - 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::(conn) - .map_err(|e| { - error!("{:?}", e); - ServiceError::RecordNotFound("Project".to_string()) - }) - } -} - -impl Message for LoadProjects { - type Result = Result, ServiceError>; -} - -impl Handler for DbExecutor { - type Result = Result, ServiceError>; - - fn handle(&mut self, msg: LoadProjects, _ctx: &mut Self::Context) -> Self::Result { - let conn = db_pool!(self); - - msg.execute(conn) - } -} diff --git a/jirs-server/src/db/tokens.rs b/jirs-server/src/db/tokens.rs deleted file mode 100644 index a14e4a8a..00000000 --- a/jirs-server/src/db/tokens.rs +++ /dev/null @@ -1,148 +0,0 @@ -use actix::{Handler, Message}; -use diesel::prelude::*; -use uuid::Uuid; - -use jirs_data::msg::WsError; -use jirs_data::{Token, UserId}; - -use crate::{ - db::{DbExecutor, DbPooledConn}, - db_pool, - errors::ServiceError, - q, -}; - -pub struct FindUserId { - pub user_id: UserId, -} - -impl FindUserId { - pub fn execute(self, conn: &DbPooledConn) -> Result { - 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); - ServiceError::Error(WsError::NoBindToken) - }) - } -} - -impl Message for FindUserId { - type Result = Result; -} - -impl Handler for DbExecutor { - type Result = Result; - - fn handle(&mut self, msg: FindUserId, _ctx: &mut Self::Context) -> Self::Result { - let conn = db_pool!(self); - msg.execute(conn) - } -} - -pub struct FindBindToken { - pub token: Uuid, -} - -impl FindBindToken { - pub fn execute(self, conn: &DbPooledConn) -> Result { - 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); - ServiceError::Error(WsError::BindTokenNotExists) - })?; - - q!(diesel::update(tokens.find(token.id)).set(bind_token.eq(None as Option))) - .execute(conn) - .map_err(|e| { - error!("{:?}", e); - ServiceError::Error(WsError::FailedToDisableBindToken) - })?; - - Ok(token) - } -} - -impl Message for FindBindToken { - type Result = Result; -} - -impl Handler for DbExecutor { - type Result = Result; - - fn handle(&mut self, msg: FindBindToken, _: &mut Self::Context) -> Self::Result { - let conn = db_pool!(self); - msg.execute(conn) - } -} - -pub struct FindAccessToken { - pub token: Uuid, -} - -impl FindAccessToken { - pub fn execute(self, conn: &DbPooledConn) -> Result { - use crate::schema::tokens::dsl::{access_token, tokens}; - - q!(tokens.filter(access_token.eq(self.token))) - .first(conn) - .map_err(|e| { - error!("{:?}", e); - ServiceError::Error(WsError::AccessTokenNotExists) - }) - } -} - -impl Message for FindAccessToken { - type Result = Result; -} - -impl Handler for DbExecutor { - type Result = Result; - - fn handle(&mut self, msg: FindAccessToken, _: &mut Self::Context) -> Self::Result { - let conn = db_pool!(self); - msg.execute(conn) - } -} - -pub struct CreateBindToken { - pub user_id: UserId, -} - -impl CreateBindToken { - pub fn execute(self, conn: &DbPooledConn) -> Result { - 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); - ServiceError::Error(WsError::FailedToCreateBindToken) - }) - } -} - -impl Message for CreateBindToken { - type Result = Result; -} - -impl Handler for DbExecutor { - type Result = Result; - - fn handle(&mut self, msg: CreateBindToken, _: &mut Self::Context) -> Self::Result { - let conn = db_pool!(self); - - msg.execute(conn) - } -} diff --git a/jirs-server/src/db/user_projects.rs b/jirs-server/src/db/user_projects.rs deleted file mode 100644 index 621743e2..00000000 --- a/jirs-server/src/db/user_projects.rs +++ /dev/null @@ -1,221 +0,0 @@ -use actix::{Handler, Message}; -use diesel::prelude::*; - -use jirs_data::msg::WsError; -use jirs_data::{ProjectId, UserId, UserProject, UserProjectId, UserRole}; - -use crate::{ - db::{DbExecutor, DbPooledConn}, - db_pool, - errors::ServiceError, - q, -}; - -pub struct CurrentUserProject { - pub user_id: UserId, -} - -impl Message for CurrentUserProject { - type Result = Result; -} - -impl Handler for DbExecutor { - type Result = Result; - - fn handle(&mut self, msg: CurrentUserProject, _: &mut Self::Context) -> Self::Result { - use crate::schema::user_projects::dsl::*; - - let conn = db_pool!(self); - - q!(user_projects.filter(user_id.eq(msg.user_id).and(is_current.eq(true)))) - .first(conn) - .map_err(|e| { - error!("{:?}", e); - ServiceError::RecordNotFound(format!("user project {}", msg.user_id)) - }) - } -} - -pub struct LoadUserProjects { - pub user_id: UserId, -} - -impl Message for LoadUserProjects { - type Result = Result, ServiceError>; -} - -impl Handler for DbExecutor { - type Result = Result, ServiceError>; - - fn handle(&mut self, msg: LoadUserProjects, _ctx: &mut Self::Context) -> Self::Result { - use crate::schema::user_projects::dsl::*; - - let conn = db_pool!(self); - - q!(user_projects.filter(user_id.eq(msg.user_id))) - .load(conn) - .map_err(|e| { - error!("{:?}", e); - ServiceError::RecordNotFound(format!("user project {}", msg.user_id)) - }) - } -} - -pub struct ChangeCurrentUserProject { - pub user_id: UserId, - pub id: UserProjectId, -} - -impl ChangeCurrentUserProject { - pub fn execute(self, conn: &DbPooledConn) -> Result { - 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); - ServiceError::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); - ServiceError::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); - ServiceError::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; -} - -impl Handler for DbExecutor { - type Result = Result; - - fn handle(&mut self, msg: ChangeCurrentUserProject, _ctx: &mut Self::Context) -> Self::Result { - let conn = db_pool!(self); - msg.execute(conn) - } -} - -pub struct RemoveInvitedUser { - pub invited_id: UserId, - pub inviter_id: UserId, - pub project_id: ProjectId, -} - -impl RemoveInvitedUser { - pub fn execute(self, conn: &DbPooledConn) -> Result { - use crate::schema::user_projects::dsl::*; - - if self.invited_id == self.inviter_id { - return Err(ServiceError::Unauthorized); - } - - q!(user_projects.filter( - user_id - .eq(self.inviter_id) - .and(project_id.eq(self.project_id)) - .and(role.eq(UserRole::Owner)), - )) - .first::(conn) - .map_err(|e| { - error!("{:?}", e); - ServiceError::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); - ServiceError::RecordNotFound(format!( - "user project user with id {} for project {}", - self.invited_id, self.project_id - )) - }) - } -} - -impl Message for RemoveInvitedUser { - type Result = Result<(), ServiceError>; -} - -impl Handler for DbExecutor { - type Result = Result<(), ServiceError>; - - fn handle(&mut self, msg: RemoveInvitedUser, _ctx: &mut Self::Context) -> Self::Result { - 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 { - 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); - ServiceError::Error(WsError::InvalidUserProject) - }) - } -} - -impl Message for CreateUserProject { - type Result = Result<(), ServiceError>; -} - -impl Handler for DbExecutor { - type Result = Result<(), ServiceError>; - - fn handle(&mut self, msg: CreateUserProject, _ctx: &mut Self::Context) -> Self::Result { - let conn = db_pool!(self); - msg.execute(conn)?; - Ok(()) - } -} diff --git a/jirs-server/src/db/users.rs b/jirs-server/src/db/users.rs deleted file mode 100644 index c9787893..00000000 --- a/jirs-server/src/db/users.rs +++ /dev/null @@ -1,442 +0,0 @@ -use actix::{Handler, Message}; -use diesel::prelude::*; -use diesel::result::Error; - -use jirs_data::{msg::WsError, EmailString, ProjectId, User, UserId, UserRole, UsernameString}; - -use crate::db::user_projects::CreateUserProject; -use crate::{ - db::{projects::CreateProject, DbExecutor, DbPooledConn}, - db_pool, - errors::ServiceError, - q, - schema::users::all_columns, -}; - -#[derive(Debug)] -pub struct FindUser { - pub user_id: UserId, -} - -impl FindUser { - pub fn execute(self, conn: &DbPooledConn) -> Result { - use crate::schema::users::dsl::*; - - q!(users.find(self.user_id)).first(conn).map_err(|e| { - error!("{:?}", e); - ServiceError::Error(WsError::UserNotExists(self.user_id)) - }) - } -} - -impl Message for FindUser { - type Result = Result; -} - -impl Handler for DbExecutor { - type Result = Result; - - fn handle(&mut self, msg: FindUser, _ctx: &mut Self::Context) -> Self::Result { - let conn = db_pool!(self); - msg.execute(conn) - } -} - -pub struct LookupUser { - pub name: String, - pub email: String, -} - -impl LookupUser { - pub fn execute(self, conn: &DbPooledConn) -> Result { - 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); - ServiceError::Error(WsError::NoMatchingPair(self.name, self.email)) - }) - } -} - -impl Message for LookupUser { - type Result = Result; -} - -impl Handler for DbExecutor { - type Result = Result; - - fn handle(&mut self, msg: LookupUser, _ctx: &mut Self::Context) -> Self::Result { - let conn = db_pool!(self); - msg.execute(conn) - } -} - -pub struct LoadProjectUsers { - pub project_id: i32, -} - -impl LoadProjectUsers { - pub fn execute(self, conn: &DbPooledConn) -> Result, ServiceError> { - 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); - ServiceError::Error(WsError::FailedToLoadProjectUsers) - }) - } -} - -impl Message for LoadProjectUsers { - type Result = Result, ServiceError>; -} - -impl Handler for DbExecutor { - type Result = Result, ServiceError>; - - fn handle(&mut self, msg: LoadProjectUsers, _ctx: &mut Self::Context) -> Self::Result { - let conn = db_pool!(self); - msg.execute(conn) - } -} - -pub struct LoadIssueAssignees { - pub issue_id: i32, -} - -impl LoadIssueAssignees { - pub fn execute(self, conn: &DbPooledConn) -> Result, ServiceError> { - 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); - ServiceError::Error(WsError::FailedToLoadAssignees) - }) - } -} - -impl Message for LoadIssueAssignees { - type Result = Result, ServiceError>; -} - -impl Handler for DbExecutor { - type Result = Result, ServiceError>; - - fn handle(&mut self, msg: LoadIssueAssignees, _ctx: &mut Self::Context) -> Self::Result { - 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 { - 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), - }; - ServiceError::Error(ws) - }) - } -} - -impl Message for CreateUser { - type Result = Result; -} - -impl Handler for DbExecutor { - type Result = Result; - - fn handle(&mut self, msg: CreateUser, _ctx: &mut Self::Context) -> Self::Result { - let conn = db_pool!(self); - msg.execute(conn) - } -} - -pub struct Register { - pub name: UsernameString, - pub email: EmailString, - pub project_id: Option, - pub role: UserRole, -} - -impl Register { - pub fn execute(self, conn: &DbPooledConn) -> Result<(), ServiceError> { - 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(ServiceError::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 { - type Result = Result<(), ServiceError>; -} - -impl Handler for DbExecutor { - type Result = Result<(), ServiceError>; - - fn handle(&mut self, msg: Register, _ctx: &mut Self::Context) -> Self::Result { - let conn = db_pool!(self); - msg.execute(conn) - } -} - -pub struct LoadInvitedUsers { - pub user_id: UserId, -} - -impl Message for LoadInvitedUsers { - type Result = Result, ServiceError>; -} - -impl Handler for DbExecutor { - type Result = Result, ServiceError>; - - fn handle(&mut self, msg: LoadInvitedUsers, _ctx: &mut Self::Context) -> Self::Result { - 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 = db_pool!(self); - - q!(users - .inner_join(invitations.on(i_email.eq(u_email))) - .filter(invited_by_id.eq(msg.user_id)) - .select(users::all_columns())) - .load(conn) - .map_err(|e| { - error!("{:?}", e); - ServiceError::Error(WsError::FailedToLoadInvitedUsers) - }) - } -} - -fn count_matching_users(name: &str, email: &str, conn: &DbPooledConn) -> i64 { - use crate::schema::users::dsl; - - 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()) - .get_result::(conn) - .unwrap_or(1) -} - -pub struct UpdateAvatarUrl { - pub user_id: UserId, - pub avatar_url: Option, -} - -impl Message for UpdateAvatarUrl { - type Result = Result; -} - -impl Handler for DbExecutor { - type Result = Result; - - fn handle(&mut self, msg: UpdateAvatarUrl, _ctx: &mut Self::Context) -> Self::Result { - use crate::schema::users::dsl::{avatar_url, id, users}; - - let conn = db_pool!(self); - - q!(diesel::update(users) - .set(avatar_url.eq(msg.avatar_url)) - .filter(id.eq(msg.user_id))) - .execute(conn) - .map_err(|e| { - error!("{:?}", e); - ServiceError::Error(WsError::FailedToChangeAvatar) - })?; - - FindUser { - user_id: msg.user_id, - } - .execute(conn) - } -} - -pub struct ProfileUpdate { - pub user_id: UserId, - pub name: String, - pub email: String, -} - -impl Message for ProfileUpdate { - type Result = Result; -} - -impl Handler for DbExecutor { - type Result = Result; - - fn handle(&mut self, msg: ProfileUpdate, _ctx: &mut Self::Context) -> Self::Result { - use crate::schema::users::dsl::{email, id, name, 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))) - .execute(conn) - .map_err(|e| ServiceError::DatabaseQueryFailed(format!("{}", e)))?; - - q!(users.find(msg.user_id)) - .first(conn) - .map_err(|e| ServiceError::DatabaseQueryFailed(format!("{}", e))) - } -} - -#[cfg(test)] -mod tests { - use diesel::connection::TransactionManager; - - use jirs_data::{Project, ProjectCategory}; - - use crate::db::build_pool; - - use super::*; - - #[test] - fn check_collision() { - use crate::schema::projects::dsl::projects; - use crate::schema::user_projects::dsl::user_projects; - use crate::schema::users::dsl::users; - - let pool = build_pool(); - let conn = &pool.get().unwrap(); - - let tm = conn.transaction_manager(); - - tm.begin_transaction(conn).unwrap(); - - diesel::delete(user_projects).execute(conn).unwrap(); - diesel::delete(users).execute(conn).unwrap(); - diesel::delete(projects).execute(conn).unwrap(); - - let project: Project = { - use crate::schema::projects::dsl::*; - - diesel::insert_into(projects) - .values(( - name.eq("baz".to_string()), - url.eq("/uz".to_string()), - description.eq("None".to_string()), - category.eq(ProjectCategory::Software), - )) - .get_result::(conn) - .unwrap() - }; - - let user: User = { - use crate::schema::users::dsl::*; - - diesel::insert_into(users) - .values(( - name.eq("Foo".to_string()), - email.eq("foo@example.com".to_string()), - )) - .get_result(conn) - .unwrap() - }; - { - use crate::schema::user_projects::dsl::*; - diesel::insert_into(user_projects) - .values(( - user_id.eq(user.id), - project_id.eq(project.id), - is_current.eq(true), - is_default.eq(true), - )) - .execute(conn) - .unwrap(); - } - - let res1 = count_matching_users("Foo", "bar@example.com", conn); - let res2 = count_matching_users("Bar", "foo@example.com", conn); - let res3 = count_matching_users("Foo", "foo@example.com", conn); - - tm.rollback_transaction(conn).unwrap(); - - assert_eq!(res1, 1); - assert_eq!(res2, 1); - assert_eq!(res3, 1); - } -} diff --git a/jirs-server/src/hi/mod.rs b/jirs-server/src/hi/mod.rs deleted file mode 100644 index 69b9b3a0..00000000 --- a/jirs-server/src/hi/mod.rs +++ /dev/null @@ -1,110 +0,0 @@ -use { - crate::errors::{HighlightError, ServiceError}, - actix::{Actor, Handler, SyncContext}, - serde::{Deserialize, Serialize}, - std::fs::*, - std::sync::Arc, - syntect::{ - easy::HighlightLines, - highlighting::{Style, ThemeSet}, - parsing::SyntaxSet, - }, -}; - -mod load; - -lazy_static::lazy_static! { - pub static ref THEME_SET: Arc = Arc::new(load::integrated_themeset()); - pub static ref SYNTAX_SET: Arc = Arc::new(load::integrated_syntaxset()); -} - -fn hi<'l>(code: &'l str, lang: &'l str) -> Result, ServiceError> { - let set = SYNTAX_SET - .as_ref() - .find_syntax_by_name(lang) - .ok_or_else(|| ServiceError::Highlight(HighlightError::UnknownLanguage))?; - let theme: &syntect::highlighting::Theme = THEME_SET - .as_ref() - .themes - .get("GitHub") - .ok_or_else(|| ServiceError::Highlight(HighlightError::UnknownTheme))?; - - let mut hi = HighlightLines::new(set, theme); - Ok(hi.highlight(code, SYNTAX_SET.as_ref())) -} - -#[derive(Debug, Default)] -pub struct HighlightActor {} - -impl Actor for HighlightActor { - type Context = SyncContext; -} - -#[derive(actix::Message)] -#[rtype(result = "Result, ServiceError>")] -pub struct HighlightCode { - pub code: String, - pub lang: String, -} - -impl Handler for HighlightActor { - type Result = Result, ServiceError>; - - fn handle(&mut self, msg: HighlightCode, _ctx: &mut Self::Context) -> Self::Result { - let res = hi(&msg.code, &msg.lang)?; - bincode::serialize(&res) - .map_err(|_| ServiceError::Highlight(HighlightError::ResultUnserializable)) - } -} - -#[derive(Serialize, Deserialize)] -pub struct Configuration { - pub port: usize, - pub bind: String, -} - -impl Default for Configuration { - fn default() -> Self { - Self { - port: std::env::var("HI_PORT") - .map_err(|_| ()) - .and_then(|s| s.parse().map_err(|_| ())) - .unwrap_or_else(|_| 6541), - bind: std::env::var("HI_BIND").unwrap_or_else(|_| "0.0.0.0".to_string()), - } - } -} - -impl Configuration { - pub fn addr(&self) -> String { - format!("{}:{}", self.bind, self.port) - } - - pub fn read() -> Self { - let contents: String = read_to_string(Self::config_file()).unwrap_or_default(); - match toml::from_str(contents.as_str()) { - Ok(config) => config, - _ => { - let config = Configuration::default(); - config.write().unwrap_or_else(|e| panic!(e)); - config - } - } - } - - pub fn write(&self) -> Result<(), String> { - let s = toml::to_string(self).map_err(|e| e.to_string())?; - write(Self::config_file(), s.as_str()).map_err(|e| e.to_string())?; - Ok(()) - } - - #[cfg(not(test))] - pub fn config_file() -> &'static str { - "highlight.toml" - } - - #[cfg(test)] - pub fn config_file() -> &'static str { - "highlight.test.toml" - } -} diff --git a/jirs-server/src/mail/mod.rs b/jirs-server/src/mail/mod.rs deleted file mode 100644 index 2559b788..00000000 --- a/jirs-server/src/mail/mod.rs +++ /dev/null @@ -1,99 +0,0 @@ -use std::fs::*; - -use actix::{Actor, SyncContext}; -// use lettre; -use serde::{Deserialize, Serialize}; - -pub mod invite; -pub mod welcome; - -pub type MailTransport = lettre::SmtpTransport; - -pub struct MailExecutor { - pub transport: MailTransport, - pub config: Configuration, -} - -impl Actor for MailExecutor { - type Context = SyncContext; -} - -impl Default for MailExecutor { - fn default() -> Self { - let config = Configuration::read(); - Self { - transport: mail_transport(&config), - config, - } - } -} - -fn mail_client(config: &Configuration) -> lettre::SmtpClient { - let mail_user = config.user.as_str(); - let mail_pass = config.pass.as_str(); - let mail_host = config.host.as_str(); - - lettre::SmtpClient::new_simple(mail_host) - .expect("Failed to init SMTP client") - .credentials(lettre::smtp::authentication::Credentials::new( - mail_user.to_string(), - mail_pass.to_string(), - )) - .connection_reuse(lettre::smtp::ConnectionReuseParameters::ReuseUnlimited) - .smtp_utf8(true) -} - -fn mail_transport(config: &Configuration) -> MailTransport { - mail_client(config).transport() -} - -#[derive(Serialize, Deserialize)] -pub struct Configuration { - pub concurrency: usize, - pub user: String, - pub pass: String, - pub host: String, - pub from: String, -} - -impl Default for Configuration { - fn default() -> Self { - Self { - concurrency: 2, - user: "apikey".to_string(), - pass: "YOUR-TOKEN".to_string(), - host: "smtp.sendgrid.net".to_string(), - from: "contact@jirs.pl".to_string(), - } - } -} - -impl Configuration { - pub fn read() -> Self { - let contents: String = read_to_string(Self::config_file()).unwrap_or_default(); - match toml::from_str(contents.as_str()) { - Ok(config) => config, - _ => { - let config = Configuration::default(); - config.write().unwrap_or_else(|e| panic!(e)); - config - } - } - } - - pub fn write(&self) -> Result<(), String> { - let s = toml::to_string(self).map_err(|e| e.to_string())?; - write(Self::config_file(), s.as_str()).map_err(|e| e.to_string())?; - Ok(()) - } - - #[cfg(not(test))] - fn config_file() -> &'static str { - "mail.toml" - } - - #[cfg(test)] - fn config_file() -> &'static str { - "mail.test.toml" - } -} diff --git a/jirs-server/src/main.rs b/jirs-server/src/main.rs index f0b87b4a..d741897b 100644 --- a/jirs-server/src/main.rs +++ b/jirs-server/src/main.rs @@ -2,81 +2,67 @@ #![feature(vec_remove_item)] #![recursion_limit = "256"] -#[macro_use] -extern crate diesel; -#[macro_use] -extern crate log; +use { + actix::Actor, + actix_web::{App, HttpServer}, +}; -use actix::Actor; -// 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 hi; -pub mod mail; -pub mod middleware; -pub mod models; -pub mod schema; -pub mod utils; -pub mod web; -pub mod ws; + +macro_rules! featured { + ($app: ident, $feature: expr, $connect: expr) => { + #[cfg(feature = $feature)] + let $app = $connect; + }; +} #[actix_rt::main] async fn main() -> Result<(), String> { dotenv::dotenv().ok(); pretty_env_logger::init(); - let web_config = web::Configuration::read(); - - std::fs::create_dir_all(web_config.tmp_dir.as_str()).map_err(|e| e.to_string())?; - #[cfg(feature = "local-storage")] - if !web_config.filesystem.is_empty() { - let filesystem = &web_config.filesystem; - std::fs::create_dir_all(filesystem.store_path.as_str()).map_err(|e| e.to_string())?; - } + let web_config = jirs_config::web::Configuration::read(); let db_addr = actix::SyncArbiter::start( - crate::db::Configuration::read().concurrency, - crate::db::DbExecutor::default, + jirs_config::database::Configuration::read().concurrency, + database_actor::DbExecutor::default, ); let mail_addr = actix::SyncArbiter::start( - crate::mail::Configuration::read().concurrency, - crate::mail::MailExecutor::default, + jirs_config::mail::Configuration::read().concurrency, + mail_actor::MailExecutor::default, + ); + let hi_addr = actix::SyncArbiter::start( + jirs_config::hi::Configuration::read().concurrency, + highlight_actor::HighlightActor::default, + ); + #[cfg(feature = "local-storage")] + let fs_addr = actix::SyncArbiter::start( + jirs_config::fs::Configuration::read().concurrency, + filesystem_actor::FileSystemExecutor::default, ); - let hi_addr = actix::SyncArbiter::start(10, crate::hi::HighlightActor::default); - let ws_server = WsServer::default().start(); + let ws_server = websocket_actor::server::WsServer::start_default(); HttpServer::new(move || { - let app = App::new() - .wrap(actix_web::middleware::Logger::default()) - .data(ws_server.clone()) - .data(db_addr.clone()) - .data(mail_addr.clone()) - .data(hi_addr.clone()) - .data(crate::db::build_pool()) - .service(crate::ws::index) - .service(actix_web::web::scope("/avatar").service(crate::web::avatar::upload)); + let app = App::new().wrap(actix_web::middleware::Logger::default()); - #[cfg(feature = "local-storage")] - let web_config = web::Configuration::read(); - #[cfg(feature = "local-storage")] - let app = if !web_config.filesystem.is_empty() { - let filesystem = &web_config.filesystem; - app.service(fs::Files::new( - filesystem.client_path.as_str(), - filesystem.store_path.as_str(), - )) - } else { - app - }; + // data step + let app = app + .data(ws_server.clone()) + .data(db_addr.clone()) + .data(mail_addr.clone()) + .data(hi_addr.clone()) + .data(database_actor::build_pool()); + featured! { app, "local-storage", app.data(fs_addr.clone()) } + ; + + // services step + let app = app + .service(websocket_actor::index) + .service(actix_web::web::scope("/avatar").service(web_actor::avatar::upload)); + + featured! { app, "local-storage", app.service(filesystem_actor::service()) } + ; app }) .workers(web_config.concurrency) diff --git a/jirs-server/src/utils.rs b/jirs-server/src/utils.rs deleted file mode 100644 index 8b137891..00000000 --- a/jirs-server/src/utils.rs +++ /dev/null @@ -1 +0,0 @@ - diff --git a/jirs-server/src/web/avatar.rs b/jirs-server/src/web/avatar.rs deleted file mode 100644 index e222c5bc..00000000 --- a/jirs-server/src/web/avatar.rs +++ /dev/null @@ -1,214 +0,0 @@ -#[cfg(feature = "aws-s3")] -use std::fs::File; -#[cfg(feature = "aws-s3")] -use std::io::Read; -use std::io::Write; - -use actix::Addr; -use actix_multipart::{Field, Multipart}; -use actix_web::http::header::ContentDisposition; -use actix_web::web::Data; -use actix_web::{post, web, Error, HttpResponse}; -use futures::executor::block_on; -use futures::{StreamExt, TryStreamExt}; -#[cfg(feature = "aws-s3")] -use rusoto_s3::{PutObjectRequest, S3Client, S3}; - -use jirs_data::{User, UserId, WsMsg}; - -use crate::db::authorize_user::AuthorizeUser; -use crate::db::user_projects::CurrentUserProject; -use crate::db::users::UpdateAvatarUrl; -use crate::db::DbExecutor; -#[cfg(feature = "aws-s3")] -use crate::web::AmazonS3Storage; -use crate::ws::InnerMsg::BroadcastToChannel; -use crate::ws::WsServer; - -#[post("/")] -pub async fn upload( - mut payload: Multipart, - db: Data>, - ws: Data>, -) -> Result { - let mut user_id: Option = None; - let mut avatar_url: Option = None; - - while let Ok(Some(field)) = payload.try_next().await { - let disposition: ContentDisposition = match field.content_disposition() { - Some(d) => d, - _ => continue, - }; - if !disposition.is_form_data() { - return Ok(HttpResponse::BadRequest().finish()); - } - match disposition.get_name() { - Some("token") => { - user_id = Some(handle_token(field, db.clone()).await?); - } - Some("avatar") => { - let id = user_id.ok_or_else(|| HttpResponse::Unauthorized().finish())?; - avatar_url = Some(handle_image(id, field, disposition, db.clone()).await?); - } - _ => continue, - }; - } - let user_id = match user_id { - Some(id) => id, - _ => return Ok(HttpResponse::Unauthorized().finish()), - }; - - let project_id = match block_on(db.send(CurrentUserProject { user_id })) { - Ok(Ok(user_project)) => user_project.project_id, - _ => return Ok(HttpResponse::UnprocessableEntity().finish()), - }; - - match (user_id, avatar_url) { - (user_id, Some(avatar_url)) => { - let user = update_user_avatar(user_id, avatar_url.clone(), db).await?; - ws.send(BroadcastToChannel( - project_id, - WsMsg::AvatarUrlChanged(user.id, avatar_url), - )) - .await - .map_err(|_| HttpResponse::UnprocessableEntity().finish())?; - Ok(HttpResponse::NoContent().finish()) - } - _ => Ok(HttpResponse::UnprocessableEntity().finish()), - } -} - -async fn update_user_avatar( - user_id: UserId, - new_url: String, - db: Data>, -) -> Result { - match db - .send(UpdateAvatarUrl { - user_id, - avatar_url: Some(new_url), - }) - .await - { - Ok(Ok(user)) => Ok(user), - - Ok(Err(e)) => { - error!("{:?}", e); - Err(HttpResponse::Unauthorized().finish().into()) - } - Err(e) => { - error!("{:?}", e); - Err(HttpResponse::Unauthorized().finish().into()) - } - } -} - -async fn handle_token(mut field: Field, db: Data>) -> Result { - let mut f: Vec = vec![]; - while let Some(chunk) = field.next().await { - let data = chunk.unwrap(); - f = web::block(move || f.write_all(&data).map(|_| f)).await?; - } - let access_token = String::from_utf8(f) - .unwrap_or_default() - .parse::() - .map_err(|_| HttpResponse::Unauthorized().finish())?; - match db.send(AuthorizeUser { access_token }).await { - Ok(Ok(user)) => Ok(user.id), - - Ok(Err(e)) => { - error!("{:?}", e); - Err(HttpResponse::Unauthorized().finish().into()) - } - Err(e) => { - error!("{:?}", e); - Err(HttpResponse::Unauthorized().finish().into()) - } - } -} - -async fn handle_image( - user_id: UserId, - mut field: Field, - disposition: ContentDisposition, - _db: Data>, -) -> Result { - let web_config = crate::web::Configuration::read(); - - let mut new_link = None; - let filename = disposition.get_filename().unwrap(); - let tmp_file_path = format!("{}/{}-{}", web_config.tmp_dir, user_id, filename); - let mut f = web::block(move || std::fs::File::create(tmp_file_path)) - .await - .unwrap(); - - // Write temp file - while let Some(chunk) = field.next().await { - let data = chunk.unwrap(); - f = web::block(move || f.write_all(&data).map(|_| f)).await?; - } - - // Write public visible file - #[cfg(feature = "local-storage")] - if !web_config.filesystem.is_empty() { - let filesystem = &web_config.filesystem; - std::fs::copy( - format!("{}/{}-{}", web_config.tmp_dir, user_id, filename), - format!("{}/{}-{}", filesystem.store_path, user_id, filename), - ) - .map_err(|_| HttpResponse::InsufficientStorage().finish())?; - - new_link = Some(format!( - "{proto}://{bind}{port}{client_path}/{user_id}-{filename}", - proto = if web_config.ssl { "https" } else { "http" }, - bind = web_config.bind, - port = match web_config.port.as_str() { - "80" | "443" => "".to_string(), - p => format!(":{}", p), - }, - client_path = filesystem.client_path, - user_id = user_id, - filename = filename - )); - } - - // Upload to AWS S3 - #[cfg(feature = "aws-s3")] - if !web_config.s3.is_empty() { - let s3 = &web_config.s3; - s3.set_variables(); - let key = format!("{}-{}", user_id, filename); - let mut tmp_file = File::open(format!("{}/{}-{}", web_config.tmp_dir, user_id, filename)) - .map_err(|_| HttpResponse::InternalServerError())?; - let mut buffer: Vec = vec![]; - tmp_file - .read_to_end(&mut buffer) - .map_err(|_| HttpResponse::InternalServerError())?; - - let client = S3Client::new(s3.region()); - let put_object = PutObjectRequest { - bucket: s3.bucket.clone(), - key: key.clone(), - body: Some(buffer.into()), - ..Default::default() - }; - let _id = client - .put_object(put_object) - .await - .map_err(|_| HttpResponse::InternalServerError())?; - new_link = Some(aws_s3_url(key.as_str(), s3)); - } - std::fs::remove_file(format!("{}/{}-{}", web_config.tmp_dir, user_id, filename).as_str()) - .unwrap_or_default(); - Ok(new_link.unwrap_or_default()) -} - -#[cfg(feature = "aws-s3")] -fn aws_s3_url(key: &str, config: &AmazonS3Storage) -> String { - format!( - "https://{bucket}.s3.{region}.amazonaws.com/{key}", - bucket = config.bucket, - region = config.region_name, - key = key - ) -} diff --git a/jirs-server/migrations/.gitkeep b/migrations/.gitkeep similarity index 100% rename from jirs-server/migrations/.gitkeep rename to migrations/.gitkeep diff --git a/jirs-server/migrations/00000000000000_diesel_initial_setup/down.sql b/migrations/00000000000000_diesel_initial_setup/down.sql similarity index 100% rename from jirs-server/migrations/00000000000000_diesel_initial_setup/down.sql rename to migrations/00000000000000_diesel_initial_setup/down.sql diff --git a/jirs-server/migrations/00000000000000_diesel_initial_setup/up.sql b/migrations/00000000000000_diesel_initial_setup/up.sql similarity index 100% rename from jirs-server/migrations/00000000000000_diesel_initial_setup/up.sql rename to migrations/00000000000000_diesel_initial_setup/up.sql diff --git a/jirs-server/migrations/2020-03-25-150001_create_uuid/down.sql b/migrations/2020-03-25-150001_create_uuid/down.sql similarity index 100% rename from jirs-server/migrations/2020-03-25-150001_create_uuid/down.sql rename to migrations/2020-03-25-150001_create_uuid/down.sql diff --git a/jirs-server/migrations/2020-03-25-150001_create_uuid/up.sql b/migrations/2020-03-25-150001_create_uuid/up.sql similarity index 100% rename from jirs-server/migrations/2020-03-25-150001_create_uuid/up.sql rename to migrations/2020-03-25-150001_create_uuid/up.sql diff --git a/jirs-server/migrations/2020-03-25-150002_create__project_category_type/down.sql b/migrations/2020-03-25-150002_create__project_category_type/down.sql similarity index 100% rename from jirs-server/migrations/2020-03-25-150002_create__project_category_type/down.sql rename to migrations/2020-03-25-150002_create__project_category_type/down.sql diff --git a/jirs-server/migrations/2020-03-25-150002_create__project_category_type/up.sql b/migrations/2020-03-25-150002_create__project_category_type/up.sql similarity index 100% rename from jirs-server/migrations/2020-03-25-150002_create__project_category_type/up.sql rename to migrations/2020-03-25-150002_create__project_category_type/up.sql diff --git a/jirs-server/migrations/2020-03-25-150003_crate__issue_priority_type/down.sql b/migrations/2020-03-25-150003_crate__issue_priority_type/down.sql similarity index 100% rename from jirs-server/migrations/2020-03-25-150003_crate__issue_priority_type/down.sql rename to migrations/2020-03-25-150003_crate__issue_priority_type/down.sql diff --git a/jirs-server/migrations/2020-03-25-150003_crate__issue_priority_type/up.sql b/migrations/2020-03-25-150003_crate__issue_priority_type/up.sql similarity index 100% rename from jirs-server/migrations/2020-03-25-150003_crate__issue_priority_type/up.sql rename to migrations/2020-03-25-150003_crate__issue_priority_type/up.sql diff --git a/jirs-server/migrations/2020-03-25-150004_create__issue_type_type/down.sql b/migrations/2020-03-25-150004_create__issue_type_type/down.sql similarity index 100% rename from jirs-server/migrations/2020-03-25-150004_create__issue_type_type/down.sql rename to migrations/2020-03-25-150004_create__issue_type_type/down.sql diff --git a/jirs-server/migrations/2020-03-25-150004_create__issue_type_type/up.sql b/migrations/2020-03-25-150004_create__issue_type_type/up.sql similarity index 100% rename from jirs-server/migrations/2020-03-25-150004_create__issue_type_type/up.sql rename to migrations/2020-03-25-150004_create__issue_type_type/up.sql diff --git a/jirs-server/migrations/2020-03-25-150005_create__issue_status/down.sql b/migrations/2020-03-25-150005_create__issue_status/down.sql similarity index 100% rename from jirs-server/migrations/2020-03-25-150005_create__issue_status/down.sql rename to migrations/2020-03-25-150005_create__issue_status/down.sql diff --git a/jirs-server/migrations/2020-03-25-150005_create__issue_status/up.sql b/migrations/2020-03-25-150005_create__issue_status/up.sql similarity index 100% rename from jirs-server/migrations/2020-03-25-150005_create__issue_status/up.sql rename to migrations/2020-03-25-150005_create__issue_status/up.sql diff --git a/jirs-server/migrations/2020-03-25-150006_create_projects/down.sql b/migrations/2020-03-25-150006_create_projects/down.sql similarity index 100% rename from jirs-server/migrations/2020-03-25-150006_create_projects/down.sql rename to migrations/2020-03-25-150006_create_projects/down.sql diff --git a/jirs-server/migrations/2020-03-25-150006_create_projects/up.sql b/migrations/2020-03-25-150006_create_projects/up.sql similarity index 100% rename from jirs-server/migrations/2020-03-25-150006_create_projects/up.sql rename to migrations/2020-03-25-150006_create_projects/up.sql diff --git a/jirs-server/migrations/2020-03-25-150007_create_users/down.sql b/migrations/2020-03-25-150007_create_users/down.sql similarity index 100% rename from jirs-server/migrations/2020-03-25-150007_create_users/down.sql rename to migrations/2020-03-25-150007_create_users/down.sql diff --git a/jirs-server/migrations/2020-03-25-150007_create_users/up.sql b/migrations/2020-03-25-150007_create_users/up.sql similarity index 100% rename from jirs-server/migrations/2020-03-25-150007_create_users/up.sql rename to migrations/2020-03-25-150007_create_users/up.sql diff --git a/jirs-server/migrations/2020-03-25-150008_create_issues/down.sql b/migrations/2020-03-25-150008_create_issues/down.sql similarity index 100% rename from jirs-server/migrations/2020-03-25-150008_create_issues/down.sql rename to migrations/2020-03-25-150008_create_issues/down.sql diff --git a/jirs-server/migrations/2020-03-25-150008_create_issues/up.sql b/migrations/2020-03-25-150008_create_issues/up.sql similarity index 100% rename from jirs-server/migrations/2020-03-25-150008_create_issues/up.sql rename to migrations/2020-03-25-150008_create_issues/up.sql diff --git a/jirs-server/migrations/2020-03-25-150009_create_comments/down.sql b/migrations/2020-03-25-150009_create_comments/down.sql similarity index 100% rename from jirs-server/migrations/2020-03-25-150009_create_comments/down.sql rename to migrations/2020-03-25-150009_create_comments/down.sql diff --git a/jirs-server/migrations/2020-03-25-150009_create_comments/up.sql b/migrations/2020-03-25-150009_create_comments/up.sql similarity index 100% rename from jirs-server/migrations/2020-03-25-150009_create_comments/up.sql rename to migrations/2020-03-25-150009_create_comments/up.sql diff --git a/jirs-server/migrations/2020-03-25-150010_create_tokens/down.sql b/migrations/2020-03-25-150010_create_tokens/down.sql similarity index 100% rename from jirs-server/migrations/2020-03-25-150010_create_tokens/down.sql rename to migrations/2020-03-25-150010_create_tokens/down.sql diff --git a/jirs-server/migrations/2020-03-25-150010_create_tokens/up.sql b/migrations/2020-03-25-150010_create_tokens/up.sql similarity index 100% rename from jirs-server/migrations/2020-03-25-150010_create_tokens/up.sql rename to migrations/2020-03-25-150010_create_tokens/up.sql diff --git a/jirs-server/migrations/2020-03-28-191306_issue_assignees/down.sql b/migrations/2020-03-28-191306_issue_assignees/down.sql similarity index 100% rename from jirs-server/migrations/2020-03-28-191306_issue_assignees/down.sql rename to migrations/2020-03-28-191306_issue_assignees/down.sql diff --git a/jirs-server/migrations/2020-03-28-191306_issue_assignees/up.sql b/migrations/2020-03-28-191306_issue_assignees/up.sql similarity index 100% rename from jirs-server/migrations/2020-03-28-191306_issue_assignees/up.sql rename to migrations/2020-03-28-191306_issue_assignees/up.sql diff --git a/jirs-server/migrations/2020-04-08-210310_change_list_position_type/down.sql b/migrations/2020-04-08-210310_change_list_position_type/down.sql similarity index 100% rename from jirs-server/migrations/2020-04-08-210310_change_list_position_type/down.sql rename to migrations/2020-04-08-210310_change_list_position_type/down.sql diff --git a/jirs-server/migrations/2020-04-08-210310_change_list_position_type/up.sql b/migrations/2020-04-08-210310_change_list_position_type/up.sql similarity index 100% rename from jirs-server/migrations/2020-04-08-210310_change_list_position_type/up.sql rename to migrations/2020-04-08-210310_change_list_position_type/up.sql diff --git a/jirs-server/migrations/2020-04-14-090059_change_project_type_field/down.sql b/migrations/2020-04-14-090059_change_project_type_field/down.sql similarity index 100% rename from jirs-server/migrations/2020-04-14-090059_change_project_type_field/down.sql rename to migrations/2020-04-14-090059_change_project_type_field/down.sql diff --git a/jirs-server/migrations/2020-04-14-090059_change_project_type_field/up.sql b/migrations/2020-04-14-090059_change_project_type_field/up.sql similarity index 100% rename from jirs-server/migrations/2020-04-14-090059_change_project_type_field/up.sql rename to migrations/2020-04-14-090059_change_project_type_field/up.sql diff --git a/jirs-server/migrations/2020-04-16-123403_add_bind_token/down.sql b/migrations/2020-04-16-123403_add_bind_token/down.sql similarity index 100% rename from jirs-server/migrations/2020-04-16-123403_add_bind_token/down.sql rename to migrations/2020-04-16-123403_add_bind_token/down.sql diff --git a/jirs-server/migrations/2020-04-16-123403_add_bind_token/up.sql b/migrations/2020-04-16-123403_add_bind_token/up.sql similarity index 100% rename from jirs-server/migrations/2020-04-16-123403_add_bind_token/up.sql rename to migrations/2020-04-16-123403_add_bind_token/up.sql diff --git a/jirs-server/migrations/2020-04-20-071751_add_roles/down.sql b/migrations/2020-04-20-071751_add_roles/down.sql similarity index 100% rename from jirs-server/migrations/2020-04-20-071751_add_roles/down.sql rename to migrations/2020-04-20-071751_add_roles/down.sql diff --git a/jirs-server/migrations/2020-04-20-071751_add_roles/up.sql b/migrations/2020-04-20-071751_add_roles/up.sql similarity index 100% rename from jirs-server/migrations/2020-04-20-071751_add_roles/up.sql rename to migrations/2020-04-20-071751_add_roles/up.sql diff --git a/jirs-server/migrations/2020-04-20-172406_create_invitations/down.sql b/migrations/2020-04-20-172406_create_invitations/down.sql similarity index 100% rename from jirs-server/migrations/2020-04-20-172406_create_invitations/down.sql rename to migrations/2020-04-20-172406_create_invitations/down.sql diff --git a/jirs-server/migrations/2020-04-20-172406_create_invitations/up.sql b/migrations/2020-04-20-172406_create_invitations/up.sql similarity index 100% rename from jirs-server/migrations/2020-04-20-172406_create_invitations/up.sql rename to migrations/2020-04-20-172406_create_invitations/up.sql diff --git a/jirs-server/migrations/2020-04-22-072211_add_bind_token_to_invitation/down.sql b/migrations/2020-04-22-072211_add_bind_token_to_invitation/down.sql similarity index 100% rename from jirs-server/migrations/2020-04-22-072211_add_bind_token_to_invitation/down.sql rename to migrations/2020-04-22-072211_add_bind_token_to_invitation/down.sql diff --git a/jirs-server/migrations/2020-04-22-072211_add_bind_token_to_invitation/up.sql b/migrations/2020-04-22-072211_add_bind_token_to_invitation/up.sql similarity index 100% rename from jirs-server/migrations/2020-04-22-072211_add_bind_token_to_invitation/up.sql rename to migrations/2020-04-22-072211_add_bind_token_to_invitation/up.sql diff --git a/jirs-server/migrations/2020-04-24-163323_add_settings/down.sql b/migrations/2020-04-24-163323_add_settings/down.sql similarity index 100% rename from jirs-server/migrations/2020-04-24-163323_add_settings/down.sql rename to migrations/2020-04-24-163323_add_settings/down.sql diff --git a/jirs-server/migrations/2020-04-24-163323_add_settings/up.sql b/migrations/2020-04-24-163323_add_settings/up.sql similarity index 100% rename from jirs-server/migrations/2020-04-24-163323_add_settings/up.sql rename to migrations/2020-04-24-163323_add_settings/up.sql diff --git a/jirs-server/migrations/2020-05-06-130610_add_custom_columns/down.sql b/migrations/2020-05-06-130610_add_custom_columns/down.sql similarity index 100% rename from jirs-server/migrations/2020-05-06-130610_add_custom_columns/down.sql rename to migrations/2020-05-06-130610_add_custom_columns/down.sql diff --git a/jirs-server/migrations/2020-05-06-130610_add_custom_columns/up.sql b/migrations/2020-05-06-130610_add_custom_columns/up.sql similarity index 100% rename from jirs-server/migrations/2020-05-06-130610_add_custom_columns/up.sql rename to migrations/2020-05-06-130610_add_custom_columns/up.sql diff --git a/jirs-server/migrations/2020-05-21-051229_multi_project_users/down.sql b/migrations/2020-05-21-051229_multi_project_users/down.sql similarity index 100% rename from jirs-server/migrations/2020-05-21-051229_multi_project_users/down.sql rename to migrations/2020-05-21-051229_multi_project_users/down.sql diff --git a/jirs-server/migrations/2020-05-21-051229_multi_project_users/up.sql b/migrations/2020-05-21-051229_multi_project_users/up.sql similarity index 100% rename from jirs-server/migrations/2020-05-21-051229_multi_project_users/up.sql rename to migrations/2020-05-21-051229_multi_project_users/up.sql diff --git a/jirs-server/migrations/2020-05-21-064702_create_messages/down.sql b/migrations/2020-05-21-064702_create_messages/down.sql similarity index 100% rename from jirs-server/migrations/2020-05-21-064702_create_messages/down.sql rename to migrations/2020-05-21-064702_create_messages/down.sql diff --git a/jirs-server/migrations/2020-05-21-064702_create_messages/up.sql b/migrations/2020-05-21-064702_create_messages/up.sql similarity index 100% rename from jirs-server/migrations/2020-05-21-064702_create_messages/up.sql rename to migrations/2020-05-21-064702_create_messages/up.sql diff --git a/jirs-server/migrations/2020-05-21-160206_add_role_to_invitation/down.sql b/migrations/2020-05-21-160206_add_role_to_invitation/down.sql similarity index 100% rename from jirs-server/migrations/2020-05-21-160206_add_role_to_invitation/down.sql rename to migrations/2020-05-21-160206_add_role_to_invitation/down.sql diff --git a/jirs-server/migrations/2020-05-21-160206_add_role_to_invitation/up.sql b/migrations/2020-05-21-160206_add_role_to_invitation/up.sql similarity index 100% rename from jirs-server/migrations/2020-05-21-160206_add_role_to_invitation/up.sql rename to migrations/2020-05-21-160206_add_role_to_invitation/up.sql diff --git a/jirs-server/migrations/2020-05-24-081604_add_message_types/down.sql b/migrations/2020-05-24-081604_add_message_types/down.sql similarity index 100% rename from jirs-server/migrations/2020-05-24-081604_add_message_types/down.sql rename to migrations/2020-05-24-081604_add_message_types/down.sql diff --git a/jirs-server/migrations/2020-05-24-081604_add_message_types/up.sql b/migrations/2020-05-24-081604_add_message_types/up.sql similarity index 100% rename from jirs-server/migrations/2020-05-24-081604_add_message_types/up.sql rename to migrations/2020-05-24-081604_add_message_types/up.sql diff --git a/jirs-server/migrations/2020-08-10-133733_add_epic_issue_type/down.sql b/migrations/2020-08-10-133733_add_epic_issue_type/down.sql similarity index 100% rename from jirs-server/migrations/2020-08-10-133733_add_epic_issue_type/down.sql rename to migrations/2020-08-10-133733_add_epic_issue_type/down.sql diff --git a/jirs-server/migrations/2020-08-10-133733_add_epic_issue_type/up.sql b/migrations/2020-08-10-133733_add_epic_issue_type/up.sql similarity index 100% rename from jirs-server/migrations/2020-08-10-133733_add_epic_issue_type/up.sql rename to migrations/2020-08-10-133733_add_epic_issue_type/up.sql diff --git a/jirs-server/migrations/2020-08-10-194809_change_epic/down.sql b/migrations/2020-08-10-194809_change_epic/down.sql similarity index 100% rename from jirs-server/migrations/2020-08-10-194809_change_epic/down.sql rename to migrations/2020-08-10-194809_change_epic/down.sql diff --git a/jirs-server/migrations/2020-08-10-194809_change_epic/up.sql b/migrations/2020-08-10-194809_change_epic/up.sql similarity index 100% rename from jirs-server/migrations/2020-08-10-194809_change_epic/up.sql rename to migrations/2020-08-10-194809_change_epic/up.sql diff --git a/jirs-server/migrations/2020-08-17-064239_add_time_boundries_to_epic/down.sql b/migrations/2020-08-17-064239_add_time_boundries_to_epic/down.sql similarity index 100% rename from jirs-server/migrations/2020-08-17-064239_add_time_boundries_to_epic/down.sql rename to migrations/2020-08-17-064239_add_time_boundries_to_epic/down.sql diff --git a/jirs-server/migrations/2020-08-17-064239_add_time_boundries_to_epic/up.sql b/migrations/2020-08-17-064239_add_time_boundries_to_epic/up.sql similarity index 100% rename from jirs-server/migrations/2020-08-17-064239_add_time_boundries_to_epic/up.sql rename to migrations/2020-08-17-064239_add_time_boundries_to_epic/up.sql diff --git a/shared/jirs-config/Cargo.toml b/shared/jirs-config/Cargo.toml new file mode 100644 index 00000000..c8c5c35f --- /dev/null +++ b/shared/jirs-config/Cargo.toml @@ -0,0 +1,39 @@ +[package] +name = "jirs-config" +version = "0.1.0" +authors = ["Adrian Wozniak "] +edition = "2018" +description = "JIRS (Simplified JIRA in Rust) shared data types" +repository = "https://gitlab.com/adrian.wozniak/jirs" +license = "MPL-2.0" +#license-file = "../LICENSE" + +[lib] +name = "jirs_config" +path = "./src/lib.rs" + +[features] +aws-s3 = ["rusoto_s3", "rusoto_core", "rusoto_signature"] +local-storage = [] +database = [] +hi = [] +mail = [] +web = ["aws-s3", "local-storage"] +websocket = [] + +[dependencies] +serde = "*" +toml = { version = "*" } + +# Amazon S3 +[dependencies.rusoto_s3] +optional = true +version = "0.45.0" + +[dependencies.rusoto_core] +optional = true +version = "0.45.0" + +[dependencies.rusoto_signature] +optional = true +version = "0.45.0" diff --git a/shared/jirs-config/src/database.rs b/shared/jirs-config/src/database.rs new file mode 100644 index 00000000..5a705d5c --- /dev/null +++ b/shared/jirs-config/src/database.rs @@ -0,0 +1,54 @@ +use std::fs::{read_to_string, write}; + +#[derive(serde::Serialize, serde::Deserialize)] +pub struct Configuration { + pub concurrency: usize, + pub database_url: String, +} + +impl Default for Configuration { + fn default() -> Self { + let database_url = if cfg!(test) { + "postgres://postgres@localhost:5432/jirs_test".to_string() + } else { + std::env::var("DATABASE_URL") + .unwrap_or_else(|_| "postgres://postgres@localhost:5432/jirs".to_string()) + }; + Self { + concurrency: 2, + database_url, + } + } +} + +impl Configuration { + pub fn read() -> Self { + let _ = std::fs::create_dir_all("./config"); + let contents: String = read_to_string(Self::config_file()).unwrap_or_default(); + match toml::from_str(contents.as_str()) { + Ok(config) => config, + _ => { + let config = Configuration::default(); + config.write().unwrap_or_else(|e| panic!(e)); + config + } + } + } + + pub fn write(&self) -> Result<(), String> { + let _ = std::fs::create_dir_all("./config"); + let s = toml::to_string(self).map_err(|e| e.to_string())?; + write(Self::config_file(), s.as_str()).map_err(|e| e.to_string())?; + Ok(()) + } + + #[cfg(not(test))] + pub fn config_file() -> &'static str { + "./config/db.toml" + } + + #[cfg(test)] + pub fn config_file() -> &'static str { + "./config/db.test.toml" + } +} diff --git a/shared/jirs-config/src/fs.rs b/shared/jirs-config/src/fs.rs new file mode 100644 index 00000000..16ea6f53 --- /dev/null +++ b/shared/jirs-config/src/fs.rs @@ -0,0 +1,64 @@ +use std::fs::{read_to_string, write}; + +use serde::{Deserialize, Serialize}; + +#[derive(Serialize, Deserialize)] +pub struct Configuration { + pub store_path: String, + pub client_path: String, + pub tmp_path: String, + pub concurrency: usize, + #[serde(default)] + pub active: bool, +} + +impl Default for Configuration { + fn default() -> Self { + Self { + store_path: "./uploads".to_string(), + client_path: "/uploads".to_string(), + tmp_path: "/tmp".to_string(), + concurrency: 2, + active: true, + } + } +} + +impl Configuration { + pub fn is_empty(&self) -> bool { + self.store_path.is_empty() + } + + pub fn read() -> Self { + let _ = std::fs::create_dir_all("./config"); + let contents: String = read_to_string(Self::config_file()).unwrap_or_default(); + let config = match toml::from_str(contents.as_str()) { + Ok(config) => config, + _ => { + let config = Configuration::default(); + config.write().unwrap_or_else(|e| panic!(e)); + config + } + }; + let _ = std::fs::create_dir_all(config.tmp_path.as_str()).map_err(|e| e.to_string()); + let _ = std::fs::create_dir_all(config.store_path.as_str()).map_err(|e| e.to_string()); + config + } + + pub fn write(&self) -> Result<(), String> { + let _ = std::fs::create_dir_all("./config"); + let s = toml::to_string(self).map_err(|e| e.to_string())?; + write(Self::config_file(), s.as_str()).map_err(|e| e.to_string())?; + Ok(()) + } + + #[cfg(not(test))] + pub fn config_file() -> &'static str { + "./config/fs.toml" + } + + #[cfg(test)] + pub fn config_file() -> &'static str { + "./config/fs.test.toml" + } +} diff --git a/shared/jirs-config/src/hi.rs b/shared/jirs-config/src/hi.rs new file mode 100644 index 00000000..583b3b0d --- /dev/null +++ b/shared/jirs-config/src/hi.rs @@ -0,0 +1,46 @@ +use std::fs::{read_to_string, write}; + +use serde::{Deserialize, Serialize}; + +#[derive(Serialize, Deserialize)] +pub struct Configuration { + pub concurrency: usize, +} + +impl Default for Configuration { + fn default() -> Self { + Self { concurrency: 2 } + } +} + +impl Configuration { + pub fn read() -> Self { + let _ = std::fs::create_dir_all("./config"); + let contents: String = read_to_string(Self::config_file()).unwrap_or_default(); + match toml::from_str(contents.as_str()) { + Ok(config) => config, + _ => { + let config = Configuration::default(); + config.write().unwrap_or_else(|e| panic!(e)); + config + } + } + } + + pub fn write(&self) -> Result<(), String> { + let _ = std::fs::create_dir_all("./config"); + let s = toml::to_string(self).map_err(|e| e.to_string())?; + write(Self::config_file(), s.as_str()).map_err(|e| e.to_string())?; + Ok(()) + } + + #[cfg(not(test))] + pub fn config_file() -> &'static str { + "./config/highlight.toml" + } + + #[cfg(test)] + pub fn config_file() -> &'static str { + "./config/highlight.test.toml" + } +} diff --git a/shared/jirs-config/src/lib.rs b/shared/jirs-config/src/lib.rs new file mode 100644 index 00000000..df75ffc1 --- /dev/null +++ b/shared/jirs-config/src/lib.rs @@ -0,0 +1,12 @@ +#[cfg(feature = "database")] +pub mod database; +#[cfg(feature = "local-storage")] +pub mod fs; +#[cfg(feature = "hi")] +pub mod hi; +#[cfg(feature = "mail")] +pub mod mail; +#[cfg(feature = "web")] +pub mod web; +#[cfg(feature = "websocket")] +pub mod websocket; diff --git a/shared/jirs-config/src/mail.rs b/shared/jirs-config/src/mail.rs new file mode 100644 index 00000000..87cbca1a --- /dev/null +++ b/shared/jirs-config/src/mail.rs @@ -0,0 +1,54 @@ +use std::fs::{read_to_string, write}; + +#[derive(serde::Serialize, serde::Deserialize)] +pub struct Configuration { + pub concurrency: usize, + pub user: String, + pub pass: String, + pub host: String, + pub from: String, +} + +impl Default for Configuration { + fn default() -> Self { + Self { + concurrency: 2, + user: "apikey".to_string(), + pass: "YOUR-TOKEN".to_string(), + host: "smtp.sendgrid.net".to_string(), + from: "contact@jirs.pl".to_string(), + } + } +} + +impl Configuration { + pub fn read() -> Self { + let _ = std::fs::create_dir_all("./config"); + let contents: String = read_to_string(Self::config_file()).unwrap_or_default(); + match toml::from_str(contents.as_str()) { + Ok(config) => config, + _ => { + let config = Configuration::default(); + config.write().unwrap_or_else(|e| panic!(e)); + config + } + } + } + + pub fn write(&self) -> Result<(), String> { + let _ = std::fs::create_dir_all("./config"); + let s = toml::to_string(self).map_err(|e| e.to_string())?; + write(Self::config_file(), s.as_str()).map_err(|e| e.to_string())?; + Ok(()) + } + + #[cfg(not(test))] + fn config_file() -> &'static str { + "./config/mail.toml" + } + + #[cfg(test)] + fn config_file() -> &'static str { + "./config/mail.test.toml" + } +} diff --git a/jirs-server/src/web/mod.rs b/shared/jirs-config/src/web.rs similarity index 54% rename from jirs-server/src/web/mod.rs rename to shared/jirs-config/src/web.rs index 8114ab7f..cfbb9041 100644 --- a/jirs-server/src/web/mod.rs +++ b/shared/jirs-config/src/web.rs @@ -1,72 +1,34 @@ #[cfg(feature = "aws-s3")] -use rusoto_core::Region; +use rusoto_signature::Region; use { - crate::{ - db::{authorize_user::AuthorizeUser, DbExecutor}, - errors::ServiceError, - middleware::authorize::token_from_headers, - }, - actix::Addr, - actix_web::{web::Data, HttpRequest, HttpResponse}, - jirs_data::User, serde::{Deserialize, Serialize}, - std::fs::*, + std::fs::{read_to_string, write}, }; -pub mod avatar; - -pub async fn user_from_request( - req: HttpRequest, - db: &Data>, -) -> Result { - let token = match token_from_headers(req.headers()) { - Ok(uuid) => uuid, - _ => return Err(ServiceError::Unauthorized.into_http_response()), - }; - match db - .send(AuthorizeUser { - access_token: token, - }) - .await - { - Ok(Ok(user)) => Ok(user), - Ok(Err(e)) => Err(e.into_http_response()), - _ => Err(ServiceError::Unauthorized.into_http_response()), - } -} - #[derive(Debug)] pub enum Protocol { Http, Https, } -#[cfg(feature = "local-storage")] -#[derive(Serialize, Deserialize)] -pub struct FileSystemStorage { - pub store_path: String, - pub client_path: String, -} - -#[cfg(feature = "local-storage")] -impl FileSystemStorage { - pub fn is_empty(&self) -> bool { - self.store_path.is_empty() - } -} - #[cfg(feature = "aws-s3")] -#[derive(Serialize, Deserialize)] +#[derive(Serialize, Deserialize, Debug)] pub struct AmazonS3Storage { pub access_key_id: String, pub secret_access_key: String, pub bucket: String, pub region_name: String, + #[serde(default = "AmazonS3Storage::default_active")] + pub active: bool, } #[cfg(feature = "aws-s3")] impl AmazonS3Storage { + pub fn default_active() -> bool { + true + } + pub fn is_empty(&self) -> bool { self.access_key_id.is_empty() || self.secret_access_key.is_empty() || self.bucket.is_empty() } @@ -88,15 +50,12 @@ pub struct Configuration { pub port: String, pub bind: String, pub ssl: bool, - pub tmp_dir: String, #[cfg(feature = "aws-s3")] pub s3: AmazonS3Storage, - #[cfg(feature = "local-storage")] - pub filesystem: FileSystemStorage, } impl Default for Configuration { - #[cfg(all(feature = "local-storage", feature = "aws-s3"))] + #[cfg(feature = "aws-s3")] fn default() -> Self { Self { concurrency: 2, @@ -104,61 +63,22 @@ impl Default for Configuration { bind: "0.0.0.0".to_string(), ssl: false, - tmp_dir: "./tmp".to_string(), - filesystem: FileSystemStorage { - store_path: "".to_string(), - client_path: "/img".to_string(), - }, s3: AmazonS3Storage { access_key_id: "".to_string(), secret_access_key: "".to_string(), bucket: "".to_string(), region_name: Region::default().name().to_string(), + active: true, }, } } - #[cfg(all(feature = "local-storage", not(feature = "aws-s3")))] + #[cfg(not(feature = "aws-s3"))] fn default() -> Self { Self { concurrency: 2, port: "5000".to_string(), bind: "0.0.0.0".to_string(), ssl: false, - - tmp_dir: "./tmp".to_string(), - filesystem: FileSystemStorage { - store_path: "./img".to_string(), - client_path: "/img".to_string(), - }, - } - } - - #[cfg(all(feature = "aws-s3", not(feature = "local-storage")))] - fn default() -> Self { - Self { - concurrency: 2, - port: "5000".to_string(), - bind: "0.0.0.0".to_string(), - ssl: false, - - tmp_dir: "./tmp".to_string(), - s3: AmazonS3Storage { - access_key_id: "".to_string(), - secret_access_key: "".to_string(), - bucket: "".to_string(), - region_name: Region::default().name().to_string(), - }, - } - } - - #[cfg(all(not(feature = "aws-s3"), not(feature = "local-storage")))] - fn default() -> Self { - Self { - concurrency: 2, - port: "5000".to_string(), - bind: "0.0.0.0".to_string(), - ssl: false, - tmp_dir: "./tmp".to_string(), } } } @@ -188,6 +108,7 @@ impl Configuration { } pub fn read() -> Self { + let _ = std::fs::create_dir_all("./config"); let contents: String = read_to_string(Self::config_file()).unwrap_or_default(); match toml::from_str(contents.as_str()) { Ok(config) => config, @@ -200,6 +121,7 @@ impl Configuration { } pub fn write(&self) -> Result<(), String> { + let _ = std::fs::create_dir_all("./config"); let s = toml::to_string(self).map_err(|e| e.to_string())?; write(Self::config_file(), s.as_str()).map_err(|e| e.to_string())?; Ok(()) @@ -207,11 +129,11 @@ impl Configuration { #[cfg(not(test))] pub fn config_file() -> &'static str { - "web.toml" + "./config/web.toml" } #[cfg(test)] pub fn config_file() -> &'static str { - "web.test.toml" + "./config/web.test.toml" } } diff --git a/shared/jirs-config/src/websocket.rs b/shared/jirs-config/src/websocket.rs new file mode 100644 index 00000000..75e563aa --- /dev/null +++ b/shared/jirs-config/src/websocket.rs @@ -0,0 +1,44 @@ +use std::fs::{read_to_string, write}; + +#[derive(serde::Serialize, serde::Deserialize)] +pub struct Configuration { + pub concurrency: usize, +} + +impl Default for Configuration { + fn default() -> Self { + Self { concurrency: 2 } + } +} + +impl Configuration { + pub fn read() -> Self { + let _ = std::fs::create_dir_all("./config"); + let contents: String = read_to_string(Self::config_file()).unwrap_or_default(); + match toml::from_str(contents.as_str()) { + Ok(config) => config, + _ => { + let config = Configuration::default(); + config.write().unwrap_or_else(|e| panic!(e)); + config + } + } + } + + pub fn write(&self) -> Result<(), String> { + let _ = std::fs::create_dir_all("./config"); + let s = toml::to_string(self).map_err(|e| e.to_string())?; + write(Self::config_file(), s.as_str()).map_err(|e| e.to_string())?; + Ok(()) + } + + #[cfg(not(test))] + pub fn config_file() -> &'static str { + "./config/websocket.toml" + } + + #[cfg(test)] + pub fn config_file() -> &'static str { + "./config/websocket.test.toml" + } +} diff --git a/jirs-data/Cargo.toml b/shared/jirs-data/Cargo.toml similarity index 78% rename from jirs-data/Cargo.toml rename to shared/jirs-data/Cargo.toml index 8e56692c..c6ad45f9 100644 --- a/jirs-data/Cargo.toml +++ b/shared/jirs-data/Cargo.toml @@ -19,10 +19,12 @@ frontend = [] [dependencies] serde = "*" serde_json = "*" -chrono = { version = "*", features = [ "serde" ] } +chrono = { version = "*", features = ["serde"] } uuid = { version = ">=0.7.0, <0.9.0", features = ["serde"] } +actix = { version = "0.10.0" } + [dependencies.diesel] optional = true version = "1.4.5" -features = [ "unstable", "postgres", "numeric", "extras", "uuidv07" ] +features = ["unstable", "postgres", "numeric", "extras", "uuidv07"] diff --git a/jirs-data/LICENSE b/shared/jirs-data/LICENSE similarity index 100% rename from jirs-data/LICENSE rename to shared/jirs-data/LICENSE diff --git a/jirs-data/src/fields.rs b/shared/jirs-data/src/fields.rs similarity index 100% rename from jirs-data/src/fields.rs rename to shared/jirs-data/src/fields.rs diff --git a/jirs-data/src/lib.rs b/shared/jirs-data/src/lib.rs similarity index 80% rename from jirs-data/src/lib.rs rename to shared/jirs-data/src/lib.rs index c001dede..da8917c1 100644 --- a/jirs-data/src/lib.rs +++ b/shared/jirs-data/src/lib.rs @@ -25,6 +25,7 @@ pub trait ToVec { fn ordered() -> Vec; } +pub type NumberOfDeleted = usize; pub type IssueId = i32; pub type ProjectId = i32; pub type UserId = i32; @@ -41,10 +42,37 @@ pub type EmailString = String; pub type UsernameString = String; pub type TitleString = String; pub type NameString = String; +pub type AvatarUrl = String; + +pub type Code = String; +pub type Lang = String; pub type BindToken = Uuid; pub type InvitationToken = Uuid; +macro_rules! enum_to_u32 { + ($kind: ident, $fallback: ident, $($e: ident => $v: expr),+) => { + #[cfg(feature = "frontend")] + impl Into for $kind { + fn into(self) -> u32 { + match self { + $($kind :: $e => $v),+ + } + } + } + + #[cfg(feature = "frontend")] + impl Into<$kind> for u32 { + fn into(self) -> $kind { + match self { + $($v => $kind :: $e),+, + _else => $kind :: $fallback, + } + } + } + } +} + #[cfg_attr(feature = "backend", derive(FromSqlRow, AsExpression))] #[cfg_attr(feature = "backend", sql_type = "IssueTypeType")] #[derive(Clone, Copy, Deserialize, Serialize, Debug, PartialOrd, PartialEq, Hash)] @@ -80,27 +108,11 @@ impl IssueType { } } -#[cfg(feature = "frontend")] -impl Into for IssueType { - fn into(self) -> u32 { - match self { - IssueType::Task => 1, - IssueType::Bug => 2, - IssueType::Story => 3, - } - } -} - -#[cfg(feature = "frontend")] -impl Into for u32 { - fn into(self) -> IssueType { - match self { - 1 => IssueType::Task, - 2 => IssueType::Bug, - 3 => IssueType::Story, - _ => IssueType::Task, - } - } +enum_to_u32! { + IssueType, Task, + Task => 1, + Bug => 2, + Story => 3 } impl IssueType { @@ -183,30 +195,14 @@ impl std::fmt::Display for IssuePriority { } } -impl Into for IssuePriority { - fn into(self) -> u32 { - match self { - IssuePriority::Highest => 5, - IssuePriority::High => 4, - IssuePriority::Medium => 3, - IssuePriority::Low => 2, - IssuePriority::Lowest => 1, - } - } -} - -impl Into for u32 { - fn into(self) -> IssuePriority { - match self { - 5 => IssuePriority::Highest, - 4 => IssuePriority::High, - 3 => IssuePriority::Medium, - 2 => IssuePriority::Low, - 1 => IssuePriority::Lowest, - _ => IssuePriority::Medium, - } - } -} +enum_to_u32!( + IssuePriority, Medium, + Highest => 5, + High => 4, + Medium => 3, + Low => 2, + Lowest => 1 +); #[cfg_attr(feature = "backend", derive(FromSqlRow, AsExpression))] #[cfg_attr(feature = "backend", sql_type = "UserRoleType")] @@ -275,26 +271,12 @@ impl std::fmt::Display for UserRole { } } -impl Into for UserRole { - fn into(self) -> u32 { - match self { - UserRole::User => 0, - UserRole::Manager => 1, - UserRole::Owner => 2, - } - } -} - -impl Into for u32 { - fn into(self) -> UserRole { - match self { - 0 => UserRole::User, - 1 => UserRole::Manager, - 2 => UserRole::Owner, - _ => UserRole::User, - } - } -} +enum_to_u32!( + UserRole, User, + User => 0, + Manager => 1, + Owner => 2 +); #[cfg_attr(feature = "backend", derive(FromSqlRow, AsExpression))] #[cfg_attr(feature = "backend", sql_type = "ProjectCategoryType")] @@ -352,26 +334,12 @@ impl std::fmt::Display for ProjectCategory { } } -impl Into for ProjectCategory { - fn into(self) -> u32 { - match self { - ProjectCategory::Software => 0, - ProjectCategory::Marketing => 1, - ProjectCategory::Business => 2, - } - } -} - -impl Into for u32 { - fn into(self) -> ProjectCategory { - match self { - 0 => ProjectCategory::Software, - 1 => ProjectCategory::Marketing, - 2 => ProjectCategory::Business, - _ => ProjectCategory::Software, - } - } -} +enum_to_u32!( + ProjectCategory, Software, + Software => 0, + Marketing => 1, + Business => 2 +); #[cfg_attr(feature = "backend", derive(FromSqlRow, AsExpression))] #[cfg_attr(feature = "backend", sql_type = "InvitationStateType")] @@ -407,26 +375,12 @@ pub enum TimeTracking { Hourly, } -impl Into for TimeTracking { - fn into(self) -> u32 { - match self { - TimeTracking::Untracked => 0, - TimeTracking::Fibonacci => 1, - TimeTracking::Hourly => 2, - } - } -} - -impl Into for u32 { - fn into(self) -> TimeTracking { - match self { - 0 => TimeTracking::Untracked, - 1 => TimeTracking::Fibonacci, - 2 => TimeTracking::Hourly, - _ => TimeTracking::Untracked, - } - } -} +enum_to_u32!( + TimeTracking, Untracked, + Untracked => 0, + Fibonacci => 1, + Hourly => 2 +); #[derive(Clone, Serialize, Debug, PartialEq)] pub struct ErrorResponse { @@ -568,26 +522,12 @@ pub enum MessageType { Mention, } -impl Into for MessageType { - fn into(self) -> u32 { - match self { - MessageType::ReceivedInvitation => 0, - MessageType::AssignedToIssue => 1, - MessageType::Mention => 2, - } - } -} - -impl Into for u32 { - fn into(self) -> MessageType { - match self { - 0 => MessageType::ReceivedInvitation, - 1 => MessageType::AssignedToIssue, - 2 => MessageType::Mention, - _ => MessageType::Mention, - } - } -} +enum_to_u32!( + MessageType, Mention, + ReceivedInvitation => 0, + AssignedToIssue => 1, + Mention => 2 +); impl std::fmt::Display for MessageType { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { diff --git a/jirs-data/src/msg.rs b/shared/jirs-data/src/msg.rs similarity index 89% rename from jirs-data/src/msg.rs rename to shared/jirs-data/src/msg.rs index 25a5763a..2557c5d3 100644 --- a/jirs-data/src/msg.rs +++ b/shared/jirs-data/src/msg.rs @@ -2,11 +2,11 @@ use serde::{Deserialize, Serialize}; use uuid::Uuid; use crate::{ - BindToken, Comment, CommentId, CreateCommentPayload, CreateIssuePayload, EmailString, Epic, - EpicId, Invitation, InvitationId, InvitationToken, Issue, IssueFieldId, IssueId, IssueStatus, - IssueStatusId, Message, MessageId, NameString, PayloadVariant, Position, Project, TitleString, - UpdateCommentPayload, UpdateProjectPayload, User, UserId, UserProject, UserProjectId, UserRole, - UsernameString, + AvatarUrl, BindToken, Code, Comment, CommentId, CreateCommentPayload, CreateIssuePayload, + EmailString, Epic, EpicId, Invitation, InvitationId, InvitationToken, Issue, IssueFieldId, + IssueId, IssueStatus, IssueStatusId, Lang, Message, MessageId, NameString, NumberOfDeleted, + PayloadVariant, Position, Project, TitleString, UpdateCommentPayload, UpdateProjectPayload, + User, UserId, UserProject, UserProjectId, UserRole, UsernameString, }; #[derive(Serialize, Deserialize, Debug, Clone, PartialEq)] @@ -181,7 +181,7 @@ pub enum WsMsg { IssueUpdate(IssueId, IssueFieldId, PayloadVariant), IssueUpdated(Issue), IssueDelete(IssueId), - IssueDeleted(IssueId), + IssueDeleted(IssueId, NumberOfDeleted), IssueCreate(CreateIssuePayload), IssueCreated(Issue), @@ -193,7 +193,7 @@ pub enum WsMsg { IssueStatusCreate(TitleString, Position), IssueStatusCreated(IssueStatus), IssueStatusDelete(IssueStatusId), - IssueStatusDeleted(IssueStatusId), + IssueStatusDeleted(IssueStatusId, NumberOfDeleted), // comments IssueCommentsLoad(IssueId), @@ -203,10 +203,10 @@ pub enum WsMsg { CommentUpdate(UpdateCommentPayload), CommentUpdated(Comment), CommentDelete(CommentId), - CommentDeleted(CommentId), + CommentDeleted(CommentId, NumberOfDeleted), // users - AvatarUrlChanged(UserId, String), + AvatarUrlChanged(UserId, AvatarUrl), ProfileUpdate(EmailString, UsernameString), ProfileUpdated, @@ -221,7 +221,7 @@ pub enum WsMsg { MessagesLoad, MessagesLoaded(Vec), MessageMarkSeen(MessageId), - MessageMarkedSeen(MessageId), + MessageMarkedSeen(MessageId, NumberOfDeleted), // epics EpicsLoad, @@ -231,7 +231,11 @@ pub enum WsMsg { EpicUpdate(EpicId, NameString), EpicUpdated(Epic), EpicDelete(EpicId), - EpicDeleted(EpicId), + EpicDeleted(EpicId, NumberOfDeleted), + + // highlight + HighlightCode(Lang, Code), + HighlightedCode(Code), // errors Error(WsError), diff --git a/jirs-data/src/payloads.rs b/shared/jirs-data/src/payloads.rs similarity index 100% rename from jirs-data/src/payloads.rs rename to shared/jirs-data/src/payloads.rs diff --git a/jirs-data/src/sql.rs b/shared/jirs-data/src/sql.rs similarity index 100% rename from jirs-data/src/sql.rs rename to shared/jirs-data/src/sql.rs