From cb26393fcdb0778bb9692833830f0086689eb9b4 Mon Sep 17 00:00:00 2001 From: Rohan Sircar Date: Sat, 9 May 2020 12:07:08 +0530 Subject: [PATCH] Many updates Split codebase into modules Added custom all in one error type Added password to user model Added password hashing using bcrypt Added model validation for username using regex --- Cargo.lock | 350 +++++++++++++----- Cargo.toml | 91 +++-- README.md | 1 + .../2020-05-02-115427_create_users/up.sql | 6 +- src/actions.rs | 69 +--- src/actions/users.rs | 83 +++++ src/errors.rs | 2 + src/errors/domain_error.rs | 88 +++++ src/main.rs | 188 ++-------- src/middlewares.rs | 3 + src/middlewares/csrf.rs | 276 ++++++++++++++ src/models.rs | 28 +- src/models/errors.rs | 13 + src/models/roles.rs | 0 src/models/users.rs | 38 ++ src/routes.rs | 2 + src/routes/auth.rs | 45 +++ src/routes/users.rs | 92 ++--- src/schema.rs | 2 + src/utils.rs | 4 + src/utils/auth.rs | 14 + src/utils/regexs.rs | 5 + test.db | Bin 4096 -> 4096 bytes 23 files changed, 983 insertions(+), 417 deletions(-) create mode 100644 src/actions/users.rs create mode 100644 src/errors.rs create mode 100644 src/errors/domain_error.rs create mode 100644 src/middlewares.rs create mode 100644 src/middlewares/csrf.rs create mode 100644 src/models/errors.rs create mode 100644 src/models/roles.rs create mode 100644 src/models/users.rs create mode 100644 src/routes/auth.rs create mode 100644 src/utils.rs create mode 100644 src/utils/auth.rs create mode 100644 src/utils/regexs.rs diff --git a/Cargo.lock b/Cargo.lock index f40f500..6370737 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -45,22 +45,32 @@ dependencies = [ "actix-service", "actix-web", "actix-web-httpauth", + "bcrypt", "bytes", + "chrono", + "comp", + "custom_error", "diesel", "dotenv", "env_logger 0.7.1", "futures", "json", "jsonwebtoken", + "lazy-regex", + "lazy_static", "listenfd", "log", "nanoid", "r2d2", "rand 0.7.3", + "regex", + "rusqlite", "serde", "serde_json", + "timeago", "uuid 0.8.1", "validator", + "validator_derive", "yarte", "yarte_helpers", ] @@ -501,6 +511,25 @@ version = "0.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b41b7ea54a0c9d92199de89e20e58d49f02f8e699814ef3fdf266f6f748d15c7" +[[package]] +name = "base64" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53d1ccbaf7d9ec9537465a97bf19edc1a4e158ecb49fc16178202238c569cc42" + +[[package]] +name = "bcrypt" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6f02d7d008a57bcb2251ba115b803934e02315edbde9a861c88713493e381b63" +dependencies = [ + "base64 0.12.1", + "blowfish", + "byteorder", + "lazy_static", + "rand 0.7.3", +] + [[package]] name = "bincode" version = "1.2.1" @@ -551,6 +580,26 @@ dependencies = [ "constant_time_eq", ] +[[package]] +name = "block-cipher-trait" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1c924d49bd09e7c06003acda26cd9742e796e34282ec6c1189404dee0c1f4774" +dependencies = [ + "generic-array", +] + +[[package]] +name = "blowfish" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6aeb80d00f2688459b8542068abd974cfb101e7a82182414a99b5026c0d85cc3" +dependencies = [ + "block-cipher-trait", + "byteorder", + "opaque-debug", +] + [[package]] name = "brotli-sys" version = "0.3.2" @@ -627,6 +676,7 @@ checksum = "80094f509cf8b5ae86a4966a39b3ff66cd7e2a3e594accec3743ff3fabeab5b2" dependencies = [ "num-integer", "num-traits", + "serde", "time", ] @@ -677,6 +727,12 @@ dependencies = [ "bitflags", ] +[[package]] +name = "comp" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b7b6cae29f71a26f0dae0e291da438d6fced0e22e78aa1484cbbc085b5170949" + [[package]] name = "console" version = "0.10.3" @@ -735,6 +791,12 @@ dependencies = [ "lazy_static", ] +[[package]] +name = "custom_error" +version = "1.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93a0fc65739ae998afc8d68e64bdac2efd1bc4ffa1a0703d171ef2defae3792f" + [[package]] name = "darling" version = "0.10.2" @@ -813,6 +875,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "33d7ca63eb2efea87a7f56a283acc49e2ce4b2bd54adf7465dc1d81fef13d8fc" dependencies = [ "byteorder", + "chrono", "diesel_derives", "libsqlite3-sys", "r2d2", @@ -1029,6 +1092,18 @@ dependencies = [ "synstructure", ] +[[package]] +name = "fallible-iterator" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4443176a9f2c162692bd3d352d745ef9413eec5782a80d8fd6f8a1ac692a07f7" + +[[package]] +name = "fallible-streaming-iterator" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7360491ce676a36bf9bb3c56c1aa791658183a54d2744120f27285738d90465a" + [[package]] name = "flate2" version = "1.0.14" @@ -1179,6 +1254,15 @@ dependencies = [ "byteorder", ] +[[package]] +name = "generic-array" +version = "0.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c68f0274ae0e023facc3c97b2e00f076be70e254bc851d972503b328db79b2ec" +dependencies = [ + "typenum", +] + [[package]] name = "getrandom" version = "0.1.14" @@ -1198,9 +1282,9 @@ checksum = "9b919933a397b79c37e33b77bb2aa3dc8eb6e165ad809e58ff75bc7db2e34574" [[package]] name = "h2" -version = "0.2.4" +version = "0.2.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "377038bf3c89d18d6ca1431e7a5027194fbd724ca10592b9487ede5e8e144f42" +checksum = "79b7246d7e4b979c03fa093da39cfb3617a96bbeee6310af63991668d7e843ff" dependencies = [ "bytes", "fnv", @@ -1287,6 +1371,12 @@ dependencies = [ "unicode-normalization", ] +[[package]] +name = "if_chain" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3360c7b59e5ffa2653671fb74b4741a5d343c03f331c0a4aeda42b5c2b0ec7d" + [[package]] name = "indexmap" version = "1.3.2" @@ -1317,6 +1407,16 @@ dependencies = [ "winreg", ] +[[package]] +name = "isolang" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "265ef164908329e47e753c769b14cbb27434abf0c41984dca201484022f09ce5" +dependencies = [ + "phf 0.7.24", + "phf_codegen 0.7.24", +] + [[package]] name = "itoa" version = "0.4.5" @@ -1368,6 +1468,15 @@ version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a91d884b6667cd606bb5a69aa0c99ba811a115fc68915e7056ec08a46e93199a" +[[package]] +name = "lazy-regex" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "03d91276c62198fd9dd1be0d8a4ed647d0a51d5d6a0679dc324dd0b499d024ff" +dependencies = [ + "lazy_static", +] + [[package]] name = "lazy_static" version = "1.4.0" @@ -1402,6 +1511,7 @@ version = "0.17.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "56d90181c2904c287e5390186be820e5ef311a3c62edebb7d6ca3d6a48ce041d" dependencies = [ + "cc", "pkg-config", "vcpkg", ] @@ -1417,9 +1527,9 @@ dependencies = [ [[package]] name = "linked-hash-map" -version = "0.5.2" +version = "0.5.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ae91b68aebc4ddb91978b11a1b02ddd8602a05ec19002801c5666000e05e0f83" +checksum = "8dd5a6d5999d9907cda8ed67bbd137d3af8085216c2ac62de5be860bd41f304a" [[package]] name = "listenfd" @@ -1472,8 +1582,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "aae38d669396ca9b707bfc3db254bc382ddb94f57cc5c235f34623a669a01dab" dependencies = [ "log", - "phf", - "phf_codegen", + "phf 0.8.0", + "phf_codegen 0.8.0", "serde", "serde_derive", "serde_json", @@ -1672,6 +1782,12 @@ dependencies = [ "pkg-config", ] +[[package]] +name = "opaque-debug" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2839e79665f131bdb5782e51f2c6c9599c133c6098982a54c794358bf432529c" + [[package]] name = "parking_lot" version = "0.10.2" @@ -1719,13 +1835,32 @@ version = "2.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d4fd5641d01c8f18a23da7b6fe29298ff4b55afcccdf78973b24cf3175fee32e" +[[package]] +name = "phf" +version = "0.7.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b3da44b85f8e8dfaec21adae67f95d93244b2ecf6ad2a692320598dcc8e6dd18" +dependencies = [ + "phf_shared 0.7.24", +] + [[package]] name = "phf" version = "0.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3dfb61232e34fcb633f43d12c58f83c1df82962dcdfa565a4e866ffc17dafe12" dependencies = [ - "phf_shared", + "phf_shared 0.8.0", +] + +[[package]] +name = "phf_codegen" +version = "0.7.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b03e85129e324ad4166b06b2c7491ae27fe3ec353af72e72cd1654c7225d517e" +dependencies = [ + "phf_generator 0.7.24", + "phf_shared 0.7.24", ] [[package]] @@ -1734,8 +1869,18 @@ version = "0.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cbffee61585b0411840d3ece935cce9cb6321f01c45477d30066498cd5e1a815" dependencies = [ - "phf_generator", - "phf_shared", + "phf_generator 0.8.0", + "phf_shared 0.8.0", +] + +[[package]] +name = "phf_generator" +version = "0.7.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09364cc93c159b8b06b1f4dd8a4398984503483891b0c26b867cf431fb132662" +dependencies = [ + "phf_shared 0.7.24", + "rand 0.6.5", ] [[package]] @@ -1744,33 +1889,42 @@ version = "0.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "17367f0cc86f2d25802b2c26ee58a7b23faeccf78a396094c13dced0d0182526" dependencies = [ - "phf_shared", + "phf_shared 0.8.0", "rand 0.7.3", ] +[[package]] +name = "phf_shared" +version = "0.7.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "234f71a15de2288bcb7e3b6515828d22af7ec8598ee6d24c3b526fa0a80b67a0" +dependencies = [ + "siphasher 0.2.3", +] + [[package]] name = "phf_shared" version = "0.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c00cf8b9eafe68dde5e9eaa2cef8ee84a9336a47d566ec55ca16589633b65af7" dependencies = [ - "siphasher", + "siphasher 0.3.3", ] [[package]] name = "pin-project" -version = "0.4.10" +version = "0.4.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "36e3dcd42688c05a66f841d22c5d8390d9a5d4c9aaf57b9285eae4900a080063" +checksum = "82c3bfbfb5bb42f99498c7234bbd768c220eb0cea6818259d0d18a1aa3d2595d" dependencies = [ "pin-project-internal", ] [[package]] name = "pin-project-internal" -version = "0.4.10" +version = "0.4.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f4d7346ac577ff1296e06a418e7618e22655bae834d4970cb6e39d6da8119969" +checksum = "ccbf6449dcfb18562c015526b085b8df1aa3cdab180af8ec2ebd300a3bd28f63" dependencies = [ "proc-macro2 1.0.12", "quote 1.0.4", @@ -1779,9 +1933,9 @@ dependencies = [ [[package]] name = "pin-project-lite" -version = "0.1.4" +version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "237844750cfbb86f67afe27eee600dfbbcb6188d734139b534cbfbf4f96792ae" +checksum = "f7505eeebd78492e0f6108f7171c4948dbb120ee8119d9d77d0afa5469bef67f" [[package]] name = "pin-utils" @@ -1872,15 +2026,6 @@ dependencies = [ "unicode-xid 0.2.0", ] -[[package]] -name = "proc-macro2-impersonated" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d32df793782c132f437089d84b487d617a0baac886fa8519751dca07db9266e0" -dependencies = [ - "unicode-xid 0.2.0", -] - [[package]] name = "quick-error" version = "1.2.3" @@ -1905,15 +2050,6 @@ dependencies = [ "proc-macro2 1.0.12", ] -[[package]] -name = "quote-impersonated" -version = "0.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3e3e450e6f50c99055b77f01eb206d8accbb7a1d14c2c7be2b0a829286296dc9" -dependencies = [ - "proc-macro2-impersonated", -] - [[package]] name = "r2d2" version = "0.8.8" @@ -2160,6 +2296,21 @@ dependencies = [ "winapi 0.3.8", ] +[[package]] +name = "rusqlite" +version = "0.21.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "64a656821bb6317a84b257737b7934f79c0dbb7eb694710475908280ebad3e64" +dependencies = [ + "bitflags", + "fallible-iterator", + "fallible-streaming-iterator", + "libsqlite3-sys", + "lru-cache", + "memchr", + "time", +] + [[package]] name = "rust-argon2" version = "0.7.0" @@ -2311,6 +2462,12 @@ dependencies = [ "num-traits", ] +[[package]] +name = "siphasher" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b8de496cf83d4ed58b6be86c3a275b8602f6ffe98d3024a869e124147a9a3ac" + [[package]] name = "siphasher" version = "0.3.3" @@ -2355,7 +2512,7 @@ checksum = "2940c75beb4e3bf3a494cef919a747a2cb81e52571e212bfbd185074add7208a" dependencies = [ "lazy_static", "new_debug_unreachable", - "phf_shared", + "phf_shared 0.8.0", "precomputed-hash", "serde", ] @@ -2366,8 +2523,8 @@ version = "0.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f24c8e5e19d22a726626f1a5e16fe15b132dcf21d10177fa5a45ce7962996b97" dependencies = [ - "phf_generator", - "phf_shared", + "phf_generator 0.8.0", + "phf_shared 0.8.0", "proc-macro2 1.0.12", "quote 1.0.4", ] @@ -2386,26 +2543,15 @@ checksum = "6446ced80d6c486436db5c078dde11a9f73d42b57fb273121e160b84f63d894c" [[package]] name = "syn" -version = "1.0.18" +version = "1.0.19" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "410a7488c0a728c7ceb4ad59b9567eb4053d02e8cc7f5c0e0eeeb39518369213" +checksum = "e8e5aa70697bb26ee62214ae3288465ecec0000f05182f039b477001f08f5ae7" dependencies = [ "proc-macro2 1.0.12", "quote 1.0.4", "unicode-xid 0.2.0", ] -[[package]] -name = "syn-impersonated" -version = "0.1.18" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c11664eb1aded8a1be30656bedb3c2ee3d6b594842c90ec635cf7b855a5d8478" -dependencies = [ - "proc-macro2-impersonated", - "quote-impersonated", - "unicode-xid 0.2.0", -] - [[package]] name = "synstructure" version = "0.12.3" @@ -2529,6 +2675,16 @@ dependencies = [ "winapi 0.3.8", ] +[[package]] +name = "timeago" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aff2f3f1ac92d664adfdea85496dceb8c044f66d62e7d953a059023385967cfc" +dependencies = [ + "chrono", + "isolang", +] + [[package]] name = "tokio" version = "0.2.20" @@ -2536,7 +2692,6 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "05c1d570eb1a36f0345a5ce9c6c6e665b70b73d11236912c0b477616aeec47b1" dependencies = [ "bytes", - "fnv", "futures-core", "iovec", "lazy_static", @@ -2639,6 +2794,12 @@ dependencies = [ "trust-dns-proto", ] +[[package]] +name = "typenum" +version = "1.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "373c8a200f9e67a0c95e62a4f52fbf80c23b4381c05a17845531982fa99e6b33" + [[package]] name = "unicase" version = "2.6.0" @@ -2755,12 +2916,12 @@ dependencies = [ [[package]] name = "v_eval" -version = "0.5.3" +version = "0.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c2a419cdda656c514fbb974d5634994278be348867645cb65bf21a9b0676b9c2" +checksum = "0dd8b599d797eb038d0dde9a3860aacb6bbba3bffa4ac64f807c8673820cc9d9" dependencies = [ "regex", - "syn-impersonated", + "syn", ] [[package]] @@ -2788,6 +2949,21 @@ dependencies = [ "url", ] +[[package]] +name = "validator_derive" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e668e9cd05c5009b833833aa1147e5727b5396ea401f22dd1167618eed4a10c9" +dependencies = [ + "if_chain", + "lazy_static", + "proc-macro2 1.0.12", + "quote 1.0.4", + "regex", + "syn", + "validator", +] + [[package]] name = "vcpkg" version = "0.2.8" @@ -2796,9 +2972,9 @@ checksum = "3fc439f2794e98976c88a2a2dafce96b930fe8010b0a256b3c2199a773933168" [[package]] name = "vec_map" -version = "0.8.1" +version = "0.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "05c78687fb1a80548ae3250346c3db86a80a7cdd77bda190189f2d0a0987c81a" +checksum = "f1bddf1187be692e79c5ffeab891132dfb0f236ed36a43c7ed39f1165ee20191" [[package]] name = "version_check" @@ -2988,9 +3164,9 @@ dependencies = [ [[package]] name = "yarte" -version = "0.8.3" +version = "0.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "884cff3b02a11d026c493282e598a5c990b17fa22b91fb73aad2bfeee47f4f01" +checksum = "819b7b4da6e6b0192ef2f8da43ed7d79716737a57246aff58ba75611f8083b94" dependencies = [ "yarte_derive", "yarte_helpers", @@ -2998,13 +3174,13 @@ dependencies = [ [[package]] name = "yarte_codegen" -version = "0.8.3" +version = "0.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a64e8ba00f9d77cd05b665b3515251544c5b1f4885e7e3f7ffc54ea27f52eabc" +checksum = "e3c178dcde178349d41d3b6c85353614fa43c2ddceba3e86073a864454149165" dependencies = [ - "proc-macro2-impersonated", - "quote-impersonated", - "syn-impersonated", + "proc-macro2 1.0.12", + "quote 1.0.4", + "syn", "yarte_dom", "yarte_helpers", "yarte_hir", @@ -3013,14 +3189,14 @@ dependencies = [ [[package]] name = "yarte_derive" -version = "0.8.3" +version = "0.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "50f11ebd841875bd9b906017844640aaac1571a4a188b3f7577efd6c02616da1" +checksum = "3a549fd6831f6354c39bb83bf78d7587e9b5ff18f7d390aefc4ba89435ba114f" dependencies = [ "prettyprint", - "proc-macro2-impersonated", - "quote-impersonated", - "syn-impersonated", + "proc-macro2 1.0.12", + "quote 1.0.4", + "syn", "tempfile", "toolchain_find", "yarte_codegen", @@ -3031,13 +3207,13 @@ dependencies = [ [[package]] name = "yarte_dom" -version = "0.8.3" +version = "0.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "43cc56a6351af550001f7f7e07be3581cde4908a8ddcbba34630168afc4ebabc" +checksum = "c08abf1e4cbf721cce79c77a7da1e42b37e7321966724b094891dc0c3d1d5cda" dependencies = [ "markup5ever", - "quote-impersonated", - "syn-impersonated", + "quote 1.0.4", + "syn", "yarte_helpers", "yarte_hir", "yarte_html", @@ -3045,9 +3221,9 @@ dependencies = [ [[package]] name = "yarte_helpers" -version = "0.8.3" +version = "0.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "771250e834a31e39a6e34e48e3fd9032542d211cb3f194f9a71b603fb841a793" +checksum = "e981464e47692f747fbc3f29cf271f65ac363cde5518ae77a50617d5490b9fce" dependencies = [ "serde", "toml", @@ -3056,14 +3232,14 @@ dependencies = [ [[package]] name = "yarte_hir" -version = "0.8.3" +version = "0.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c86bad7da0c2ae56374390919fa1d7abaa067f6325a3e27865ae8598bfc24057" +checksum = "6105dad390184a8e4afdce19cdd0956ae1af057da1571d7add988527cbd59953" dependencies = [ "derive_more", - "proc-macro2-impersonated", - "quote-impersonated", - "syn-impersonated", + "proc-macro2 1.0.12", + "quote 1.0.4", + "syn", "v_eval", "v_htmlescape", "yarte_helpers", @@ -3072,9 +3248,9 @@ dependencies = [ [[package]] name = "yarte_html" -version = "0.8.3" +version = "0.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "261043d486d9ecdcb33c68d0323444db6ab6915f90e973c5cbd4331560ec0281" +checksum = "a0e8c01747147af821238b33c76c3045420a245fcfb3e2987becf2860b54598a" dependencies = [ "log", "mac", @@ -3087,15 +3263,15 @@ dependencies = [ [[package]] name = "yarte_parser" -version = "0.8.3" +version = "0.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6f855c622d29c8a8af4f4b7a3f81e83ac0be6e6074977a017c3945db05fba311" +checksum = "061ed8ddf990b97d89827c42346f1273d434e79a3618786d3f595f2f1befe418" dependencies = [ "annotate-snippets", "derive_more", - "proc-macro2-impersonated", - "quote-impersonated", - "syn-impersonated", + "proc-macro2 1.0.12", + "quote 1.0.4", + "syn", "unicode-xid 0.2.0", "yarte_helpers", ] diff --git a/Cargo.toml b/Cargo.toml index 76aba72..718c43b 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,43 +1,70 @@ [package] -name = "actix-demo" -version = "0.1.0" -authors = ["Rohan Sircar "] -edition = "2018" - -# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html +name = 'actix-demo' +version = '0.1.0' +authors = ['Rohan Sircar '] +edition = '2018' [dependencies] -actix-web = "2" -actix-rt = "1" -actix-service = "1.0.5" -actix-files = "0.2.1" +actix-web = '2' +actix-rt = '1' +actix-service = '1.0.5' +actix-files = '0.2.1' +bytes = '0.5.4' +futures = '0.3.4' +log = '0.4.8' +env_logger = '0.7' +serde_json = '1.0.52' +json = '0.12.4' +listenfd = '0.3.3' +dotenv = '0.15' +r2d2 = '0.8.8' +validator = '0.10.0' +validator_derive = '0.10' +jsonwebtoken = '7' +actix-identity = '0.2.1' +actix-web-httpauth = '0.4.1' +actix-http = '1.0.1' +rand = '0.7.3' +nanoid = '0.3.0' +bcrypt = '0.7' +timeago = '0.2.1' +comp = '0.2.1' +regex = '1.3.7' +lazy_static = '1.4.0' +lazy-regex = '0.1.2' +custom_error = '1.7.1' -bytes = "0.5.4" -futures = "0.3.4" -log = "0.4.8" -env_logger = "0.7" +[dependencies.serde] +version = '1.0.106' +features = ['derive'] -serde = { version = "1.0.106", features = ["derive"] } -serde_json = "1.0.52" -json = "0.12.4" +[dependencies.yarte] +version = '0.9.0' +features = ['html-min'] -yarte = { version = "0.8.3", features = ["html-min"] } +[dependencies.diesel] +version = '1.4.4' +features = [ + 'sqlite', + 'r2d2', + 'chrono', +] -listenfd = "0.3.3" +[dependencies.uuid] +version = '0.8' +features = [ + 'serde', + 'v4', +] -diesel = { version = "1.4.4", features = ["sqlite", "r2d2"] } -dotenv = "0.15" -r2d2 = "0.8.8" -uuid = { version = "0.8", features = ["serde", "v4"] } -validator = "0.10.0" -jsonwebtoken = "7" -actix-identity = "0.2.1" -actix-web-httpauth = "0.4.1" -actix-http = "1.0.1" -rand = "0.7.3" -nanoid = "0.3.0" +[dependencies.rusqlite] +version = '0.21.0' +features = ['bundled'] +[dependencies.chrono] +version = '0.4.11' +features = ['serde'] [build-dependencies.yarte_helpers] -version = "0.8" +version = '0.9.0' default-features = false -features = ["config"] \ No newline at end of file +features = ['config'] diff --git a/README.md b/README.md index e69de29..351fc5c 100644 --- a/README.md +++ b/README.md @@ -0,0 +1 @@ +Testing out the Rust framework Actix-Web to create a JSON API CRUD Web App. diff --git a/migrations/2020-05-02-115427_create_users/up.sql b/migrations/2020-05-02-115427_create_users/up.sql index 33212e2..e81e5c2 100644 --- a/migrations/2020-05-02-115427_create_users/up.sql +++ b/migrations/2020-05-02-115427_create_users/up.sql @@ -1,5 +1,7 @@ -- Your SQL goes here CREATE TABLE users ( - id INTEGER NOT NULL PRIMARY KEY, - name VARCHAR NOT NULL + id INTEGER PRIMARY KEY NOT NULL , + name VARCHAR NOT NULL, + password VARCHAR NOT NULL, + created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ) diff --git a/src/actions.rs b/src/actions.rs index 599dd78..b374349 100644 --- a/src/actions.rs +++ b/src/actions.rs @@ -1,67 +1,2 @@ -use diesel::prelude::*; - -use crate::models; - -pub fn find_user_by_uid( - uid: i32, - conn: &SqliteConnection, -) -> Result, diesel::result::Error> { - use crate::schema::users::dsl::*; - - let maybe_user = users.find(uid).first::(conn).optional(); - - // Ok(user) - maybe_user -} - -pub fn find_user_by_name( - user_name: String, - conn: &SqliteConnection, -) -> Result, diesel::result::Error> { - use crate::schema::users::dsl::*; - - let maybe_user = users - .filter(name.eq(user_name)) - .first::(conn) - .optional(); - - maybe_user -} - -pub fn get_all( - conn: &SqliteConnection, -) -> Result>, diesel::result::Error> { - use crate::schema::users::dsl::*; - users.load::(conn).optional() -} - -/// Run query using Diesel to insert a new database row and return the result. -pub fn insert_new_user( - nu: &models::NewUser, - conn: &SqliteConnection, -) -> Result { - // It is common when using Diesel with Actix web to import schema-related - // modules inside a function's scope (rather than the normal module's scope) - // to prevent import collisions and namespace pollution. - use crate::schema::users::dsl::*; - - // let new_user = models::User { - // id: Uuid::new_v4().to_string(), - // name: nu.name.to_string(), - // }; - - // let x = users.load::(conn).optional(); - // let target = users.find("4"); - // let test_user = models::User { - // id: "5".to_owned(), - // name: "who".to_owned(), - // }; - // let update_result = diesel::update(target).set(&test_user).execute(conn); - - diesel::insert_into(users).values(nu).execute(conn)?; - let user = users - .filter(name.eq(nu.name.clone())) - .first::(conn); - user - // Ok(nu.clone()) -} +pub mod users; +pub use self::users::*; diff --git a/src/actions/users.rs b/src/actions/users.rs new file mode 100644 index 0000000..f9b6cbf --- /dev/null +++ b/src/actions/users.rs @@ -0,0 +1,83 @@ +use diesel::prelude::*; + +use crate::errors; +use crate::models; +use bcrypt::{hash, DEFAULT_COST}; +use std::rc::Rc; + +pub fn find_user_by_uid( + uid: i32, + conn: &SqliteConnection, +) -> Result, errors::DomainError> { + use crate::schema::users::dsl::*; + + let maybe_user = users + .select((name, created_at)) + .find(uid) + .first::(conn) + .optional(); + + Ok(maybe_user?) +} + +pub fn _find_user_by_name( + user_name: String, + conn: &SqliteConnection, +) -> Result, errors::DomainError> { + use crate::schema::users::dsl::*; + + let maybe_user = users + .select((name, created_at)) + .filter(name.eq(user_name)) + .first::(conn) + .optional(); + + Ok(maybe_user?) +} + +pub fn get_all( + conn: &SqliteConnection, +) -> Result>, errors::DomainError> { + use crate::schema::users::dsl::*; + Ok(users + .select((name, created_at)) + .load::(conn) + .optional()?) +} + +/// Run query using Diesel to insert a new database row and return the result. +pub fn insert_new_user( + mut nu: Rc, + conn: &SqliteConnection, +) -> Result { + // It is common when using Diesel with Actix web to import schema-related + // modules inside a function's scope (rather than the normal module's scope) + // to prevent import collisions and namespace pollution. + use crate::schema::users::dsl::*; + + // let new_user = models::User { + // id: Uuid::new_v4().to_string(), + // name: nu.name.to_string(), + // }; + + // let x = users.load::(conn).optional(); + // let target = users.find("4"); + // let test_user = models::User { + // id: "5".to_owned(), + // name: "who".to_owned(), + // }; + // let update_result = diesel::update(target).set(&test_user).execute(conn); + + // let mut nu2 = nu.clone(); + let mut nu2 = Rc::make_mut(&mut nu); + nu2.password = hash(nu2.password.clone(), DEFAULT_COST)?; + + diesel::insert_into(users) + .values(nu.as_ref()) + .execute(conn)?; + let user = users + .select((name, created_at)) + .filter(name.eq(nu.name.clone())) + .first::(conn); + Ok(user?) +} diff --git a/src/errors.rs b/src/errors.rs new file mode 100644 index 0000000..2f87a60 --- /dev/null +++ b/src/errors.rs @@ -0,0 +1,2 @@ +pub mod domain_error; +pub use self::domain_error::*; diff --git a/src/errors/domain_error.rs b/src/errors/domain_error.rs new file mode 100644 index 0000000..118e3be --- /dev/null +++ b/src/errors/domain_error.rs @@ -0,0 +1,88 @@ +use actix_web::{HttpResponse, ResponseError}; +use bcrypt::BcryptError; +use custom_error::custom_error; +// use derive_more::Display; +// use diesel::result::DatabaseErrorKind; +use crate::models::errors::*; +use r2d2; +use std::convert::From; +// use std::error::Error; + +// pub enum DomainError { +// #[display(fmt = "PasswordHashError")] +// PwdHashError, +// #[display(fmt = "Bad Id")] +// IdError, +// #[display(fmt = "Generic Error")] +// GenericError, +// DuplicateValue, +// } + +// impl Error for DomainError { +// fn source(&self) -> Option<&(dyn error::Error + 'static)> { +// // Generic error, underlying cause isn't tracked. +// None +// } +// } + +// impl From for DomainError { +// fn from(error: DBError) -> DomainError { +// // We only care about UniqueViolations +// match error { +// DBError::DatabaseError(kind, info) => { +// let message = info.details().unwrap_or_else(|| info.message()).to_string(); +// match kind { +// DatabaseErrorKind::UniqueViolation => DomainError::DuplicateValue(message), +// _ => DomainError::GenericError(message), +// } +// } +// _ => DomainError::GenericError(String::from("Some database error occured")), +// } +// } +// } + +custom_error! { pub DomainError + PwdHashError {source: BcryptError} = "Failed to has password", + DbError {source: diesel::result::Error} = "Database error", + DbPoolError {source: r2d2::Error} = "Failed to get connection from pool", + GenericError {cause: String} = "Generic Error - Reason: {cause}" +} + +impl ResponseError for DomainError { + fn error_response(&self) -> HttpResponse { + match self { + DomainError::PwdHashError { source } => { + HttpResponse::InternalServerError().json(ErrorModel { + status_code: 500, + reason: format!( + "{} {}", + "Unexpected Error - Failed to hash password", source + ), + }) + } + DomainError::DbError { source } => { + HttpResponse::InternalServerError().json(ErrorModel { + status_code: 500, + reason: format!("{} {}", "Unexpected Database Error", source), + }) + } + DomainError::DbPoolError { source } => { + HttpResponse::InternalServerError().json(ErrorModel { + status_code: 500, + reason: format!( + "{} {}", + "Unexpected Error - Failed to get connection from pool", source + ), + }) + } + DomainError::GenericError { cause } => HttpResponse::BadRequest().json(ErrorModel { + status_code: 400, + reason: format!( + "{} {}, ", + "Unexpected Database Error - ".to_owned(), + cause.clone() + ), + }), + } + } +} diff --git a/src/main.rs b/src/main.rs index 1897c2a..3ac38c4 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,32 +1,43 @@ #[macro_use] extern crate diesel; +#[macro_use] +extern crate comp; +#[macro_use] +extern crate validator_derive; +extern crate bcrypt; +extern crate custom_error; +extern crate regex; +extern crate validator; use actix_web::{ - dev::ServiceRequest, error, get, middleware, post, web, App, Error, HttpRequest, HttpResponse, - HttpServer, Responder, + error, get, middleware, post, web, App, Error, HttpRequest, HttpResponse, HttpServer, Responder, }; use yarte::Template; -use actix_web_httpauth::{extractors::basic::BasicAuth, middleware::HttpAuthentication}; +use actix_web_httpauth::middleware::HttpAuthentication; use actix_http::cookie::SameSite; -use actix_identity::{CookieIdentityPolicy, Identity, IdentityService}; +use actix_identity::{CookieIdentityPolicy, IdentityService}; use rand::Rng; -// use actix_http::*; - use actix_files as fs; use diesel::prelude::*; use diesel::r2d2::{self, ConnectionManager}; -use routes::*; +// use middlewares::csrf; +// use routes; +// use routes::users; +// use utils; mod actions; +mod errors; +mod middlewares; mod models; mod routes; mod schema; mod types; +mod utils; #[macro_use] extern crate log; @@ -68,161 +79,14 @@ impl UserServiceImpl { } } -pub trait UserService { - fn do_something(&self); -} - -impl UserService for UserServiceImpl { - fn do_something(&self) { - println!("hello"); - } -} - -fn fun1(user_service: &dyn UserService) { - user_service.do_something(); -} - -fn fun2(user_service: T) -where - T: UserService, -{ - user_service.do_something(); -} - -/// In this example validator returns immediately, -/// but since it is required to return anything -/// that implements `IntoFuture` trait, -/// it can be extended to query database -/// or to do something else in a async manner. -async fn validator(req: ServiceRequest, credentials: BasicAuth) -> Result { - // All users are great and more than welcome! - // let pool = req.app_data::(); - // let maybe_header = req.headers().get("Authorization"); - // match maybe_header { - // Some(value) => { - // info!("{:?}", *value); - // let x: Result = Scheme::parse(value); - // let y = x.expect("Error parsing header"); - // println!("{}", y.user_id()); - // println!("{:?}", y.password().clone()); - // } - // None => debug!("Header not found"), - // } - - // maybe_header - // .map(|value| { - // let x: Result = Scheme::parse(value); - // x - // }) - // .map(|maybe_basic| { - // maybe_basic - // .map(|x| { - // println!("{}", x.user_id()); - // println!("{:?}", x.password().clone()); - // }) - // .map_err(|x| println!("error parsing reason - {}", x.to_string())) - // // maybe_basic - // }); - // let auth = Authorization::; - println!("{}", credentials.user_id()); - println!("{:?}", credentials.password()); - Ok(req) -} - -// fn parse(header: &HeaderValue) -> Result { -// // "Basic *" length -// if header.len() < 7 { -// return Err(ParseError::Invalid); -// } - -// let mut parts = header.to_str()?.splitn(2, ' '); -// match parts.next() { -// Some(scheme) if scheme == "Basic" => (), -// _ => return Err(ParseError::MissingScheme), -// } - -// let decoded = base64::decode(parts.next().ok_or(ParseError::Invalid)?)?; -// let mut credentials = str::from_utf8(&decoded)?.splitn(2, ':'); - -// let user_id = credentials -// .next() -// .ok_or(ParseError::MissingField("user_id")) -// .map(|user_id| user_id.to_string().into())?; -// let password = credentials -// .next() -// .ok_or(ParseError::MissingField("password")) -// .map(|password| { -// if password.is_empty() { -// None -// } else { -// Some(password.to_string().into()) -// } -// })?; - -// Ok(Basic { user_id, password }) -// } - -#[get("/login")] -async fn login(id: Identity) -> HttpResponse { - let maybe_identity = id.identity(); - // id.remember("user1".to_owned()); - let response = if let Some(identity) = maybe_identity { - HttpResponse::Ok() - .header("location", "/") - .content_type("text/plain") - .body(format!("Already logged in {}", identity)) - } else { - id.remember("user1".to_owned()); - HttpResponse::Found().header("location", "/").finish() - }; - // HttpResponse::Found().header("location", "/").finish() - response -} - -#[get("/logout")] -async fn logout(id: Identity) -> HttpResponse { - let maybe_identity = id.identity(); - // id.remember("user1".to_owned()); - let response = if let Some(identity) = maybe_identity { - info!("Logging out {user}", user = identity); - id.forget(); - HttpResponse::Found().header("location", "/").finish() - } else { - HttpResponse::Ok() - .header("location", "/") - .content_type("text/plain") - .body("Not logged in") - }; - // id.forget(); - // HttpResponse::Found().header("location", "/").finish() - response -} - -#[get("/")] -async fn index2(id: Identity) -> String { - format!( - "Hello {}", - id.identity().unwrap_or_else(|| "Anonymous".to_owned()) - ) -} - #[actix_rt::main] async fn main() -> std::io::Result<()> { std::env::set_var("RUST_LOG", "debug"); env_logger::init(); dotenv::dotenv().ok(); - let user_service: Box = Box::new(UserServiceImpl::new()); - user_service.do_something(); - - fun1(user_service.as_ref()); - - let user_service_impl = UserServiceImpl::new(); - fun2(user_service_impl); - - let basic_auth_middleware = HttpAuthentication::basic(validator); + let basic_auth_middleware = HttpAuthentication::basic(utils::auth::validator); - // fun1(Rc::clone(&user_service).as_ref()); // set up database connection pool let connspec = std::env::var("DATABASE_URL").expect("DATABASE_URL NOT FOUND"); let manager = ConnectionManager::::new(connspec); @@ -240,18 +104,18 @@ async fn main() -> std::io::Result<()> { CookieIdentityPolicy::new(&private_key) .name("my-app-auth") .secure(false) - .same_site(SameSite::Lax), // .same_site(), + .same_site(SameSite::Lax), )) .wrap(middleware::Logger::default()) .service(web::scope("/chat").wrap(basic_auth_middleware.clone())) // .service(extract_my_obj) // .service(index) - .service(get_user) - .service(add_user) - .service(get_all_users) - .service(login) - .service(logout) - .service(index2) + .service(routes::users::get_user) + .service(routes::users::add_user) + .service(routes::users::get_all_users) + .service(routes::auth::login) + .service(routes::auth::logout) + .service(routes::auth::index) .service(fs::Files::new("/", "./static")) }; HttpServer::new(app).bind(addr)?.run().await diff --git a/src/middlewares.rs b/src/middlewares.rs new file mode 100644 index 0000000..103185b --- /dev/null +++ b/src/middlewares.rs @@ -0,0 +1,3 @@ +pub mod csrf; + +pub use self::csrf::*; diff --git a/src/middlewares/csrf.rs b/src/middlewares/csrf.rs new file mode 100644 index 0000000..4cd630a --- /dev/null +++ b/src/middlewares/csrf.rs @@ -0,0 +1,276 @@ +// //! A filter for cross-site request forgery (CSRF). +// //! +// //! This middleware is stateless and [based on request +// //! headers](https://www.owasp.org/index.php/Cross-Site_Request_Forgery_(CSRF)_Prevention_Cheat_Sheet#Verifying_Same_Origin_with_Standard_Headers). +// //! +// //! By default requests are allowed only if one of these is true: +// //! +// //! * The request method is safe (`GET`, `HEAD`, `OPTIONS`). It is the +// //! applications responsibility to ensure these methods cannot be used to +// //! execute unwanted actions. Note that upgrade requests for websockets are +// //! also considered safe. +// //! * The `Origin` header (added automatically by the browser) matches one +// //! of the allowed origins. +// //! * There is no `Origin` header but the `Referer` header matches one of +// //! the allowed origins. +// //! +// //! Use [`CsrfFilter::allow_xhr()`](struct.CsrfFilter.html#method.allow_xhr) +// //! if you want to allow requests with unprotected methods via +// //! [CORS](../cors/struct.Cors.html). +// //! +// //! # Example +// //! +// //! ``` +// //! # extern crate actix_web; +// //! use actix_web::middleware::csrf; +// //! use actix_web::{http, App, HttpRequest, HttpResponse}; +// //! +// //! fn handle_post(_: &HttpRequest) -> &'static str { +// //! "This action should only be triggered with requests from the same site" +// //! } +// //! +// //! fn main() { +// //! let app = App::new() +// //! .middleware( +// //! csrf::CsrfFilter::new().allowed_origin("https://www.example.com"), +// //! ) +// //! .resource("/", |r| { +// //! r.method(http::Method::GET).f(|_| HttpResponse::Ok()); +// //! r.method(http::Method::POST).f(handle_post); +// //! }) +// //! .finish(); +// //! } +// //! ``` +// //! +// //! In this example the entire application is protected from CSRF. + +// use std::borrow::Cow; +// use std::collections::HashSet; + +// use bytes::Bytes; +// use error::{ResponseError, Result}; +// use http::{header, HeaderMap, HttpTryFrom, Uri}; +// use httprequest::HttpRequest; +// use httpresponse::HttpResponse; +// use middleware::{Middleware, Started}; +// use server::Request; + +// /// Potential cross-site request forgery detected. +// #[derive(Debug, Fail)] +// pub enum CsrfError { +// /// The HTTP request header `Origin` was required but not provided. +// #[fail(display = "Origin header required")] +// MissingOrigin, +// /// The HTTP request header `Origin` could not be parsed correctly. +// #[fail(display = "Could not parse Origin header")] +// BadOrigin, +// /// The cross-site request was denied. +// #[fail(display = "Cross-site request denied")] +// CsrDenied, +// } + +// impl ResponseError for CsrfError { +// fn error_response(&self) -> HttpResponse { +// HttpResponse::Forbidden().body(self.to_string()) +// } +// } + +// fn uri_origin(uri: &Uri) -> Option { +// match ( +// uri.scheme_part(), +// uri.host(), +// uri.port_part().map(|port| port.as_u16()), +// ) { +// (Some(scheme), Some(host), Some(port)) => Some(format!("{}://{}:{}", scheme, host, port)), +// (Some(scheme), Some(host), None) => Some(format!("{}://{}", scheme, host)), +// _ => None, +// } +// } + +// fn origin(headers: &HeaderMap) -> Option, CsrfError>> { +// headers +// .get(header::ORIGIN) +// .map(|origin| { +// origin +// .to_str() +// .map_err(|_| CsrfError::BadOrigin) +// .map(|o| o.into()) +// }) +// .or_else(|| { +// headers.get(header::REFERER).map(|referer| { +// Uri::try_from(Bytes::from(referer.as_bytes())) +// .ok() +// .as_ref() +// .and_then(uri_origin) +// .ok_or(CsrfError::BadOrigin) +// .map(|o| o.into()) +// }) +// }) +// } + +// /// A middleware that filters cross-site requests. +// /// +// /// To construct a CSRF filter: +// /// +// /// 1. Call [`CsrfFilter::build`](struct.CsrfFilter.html#method.build) to +// /// start building. +// /// 2. [Add](struct.CsrfFilterBuilder.html#method.allowed_origin) allowed +// /// origins. +// /// 3. Call [finish](struct.CsrfFilterBuilder.html#method.finish) to retrieve +// /// the constructed filter. +// /// +// /// # Example +// /// +// /// ``` +// /// use actix_web::middleware::csrf; +// /// use actix_web::App; +// /// +// /// # fn main() { +// /// let app = App::new() +// /// .middleware(csrf::CsrfFilter::new().allowed_origin("https://www.example.com")); +// /// # } +// /// ``` +// #[derive(Default)] +// pub struct CsrfFilter { +// origins: HashSet, +// allow_xhr: bool, +// allow_missing_origin: bool, +// allow_upgrade: bool, +// } + +// impl CsrfFilter { +// /// Start building a `CsrfFilter`. +// pub fn new() -> CsrfFilter { +// CsrfFilter { +// origins: HashSet::new(), +// allow_xhr: false, +// allow_missing_origin: false, +// allow_upgrade: false, +// } +// } + +// /// Add an origin that is allowed to make requests. Will be verified +// /// against the `Origin` request header. +// pub fn allowed_origin>(mut self, origin: T) -> CsrfFilter { +// self.origins.insert(origin.into()); +// self +// } + +// /// Allow all requests with an `X-Requested-With` header. +// /// +// /// A cross-site attacker should not be able to send requests with custom +// /// headers unless a CORS policy whitelists them. Therefore it should be +// /// safe to allow requests with an `X-Requested-With` header (added +// /// automatically by many JavaScript libraries). +// /// +// /// This is disabled by default, because in Safari it is possible to +// /// circumvent this using redirects and Flash. +// /// +// /// Use this method to enable more lax filtering. +// pub fn allow_xhr(mut self) -> CsrfFilter { +// self.allow_xhr = true; +// self +// } + +// /// Allow requests if the expected `Origin` header is missing (and +// /// there is no `Referer` to fall back on). +// /// +// /// The filter is conservative by default, but it should be safe to allow +// /// missing `Origin` headers because a cross-site attacker cannot prevent +// /// the browser from sending `Origin` on unprotected requests. +// pub fn allow_missing_origin(mut self) -> CsrfFilter { +// self.allow_missing_origin = true; +// self +// } + +// /// Allow cross-site upgrade requests (for example to open a WebSocket). +// pub fn allow_upgrade(mut self) -> CsrfFilter { +// self.allow_upgrade = true; +// self +// } + +// fn validate(&self, req: &Request) -> Result<(), CsrfError> { +// let is_upgrade = req.headers().contains_key(header::UPGRADE); +// let is_safe = req.method().is_safe() && (self.allow_upgrade || !is_upgrade); + +// if is_safe || (self.allow_xhr && req.headers().contains_key("x-requested-with")) { +// Ok(()) +// } else if let Some(header) = origin(req.headers()) { +// match header { +// Ok(ref origin) if self.origins.contains(origin.as_ref()) => Ok(()), +// Ok(_) => Err(CsrfError::CsrDenied), +// Err(err) => Err(err), +// } +// } else if self.allow_missing_origin { +// Ok(()) +// } else { +// Err(CsrfError::MissingOrigin) +// } +// } +// } + +// impl Middleware for CsrfFilter { +// fn start(&self, req: &HttpRequest) -> Result { +// self.validate(req)?; +// Ok(Started::Done) +// } +// } + +// #[cfg(test)] +// mod tests { +// use super::*; +// use http::Method; +// use test::TestRequest; + +// #[test] +// fn test_safe() { +// let csrf = CsrfFilter::new().allowed_origin("https://www.example.com"); + +// let req = TestRequest::with_header("Origin", "https://www.w3.org") +// .method(Method::HEAD) +// .finish(); + +// assert!(csrf.start(&req).is_ok()); +// } + +// #[test] +// fn test_csrf() { +// let csrf = CsrfFilter::new().allowed_origin("https://www.example.com"); + +// let req = TestRequest::with_header("Origin", "https://www.w3.org") +// .method(Method::POST) +// .finish(); + +// assert!(csrf.start(&req).is_err()); +// } + +// #[test] +// fn test_referer() { +// let csrf = CsrfFilter::new().allowed_origin("https://www.example.com"); + +// let req = +// TestRequest::with_header("Referer", "https://www.example.com/some/path?query=param") +// .method(Method::POST) +// .finish(); + +// assert!(csrf.start(&req).is_ok()); +// } + +// #[test] +// fn test_upgrade() { +// let strict_csrf = CsrfFilter::new().allowed_origin("https://www.example.com"); + +// let lax_csrf = CsrfFilter::new() +// .allowed_origin("https://www.example.com") +// .allow_upgrade(); + +// let req = TestRequest::with_header("Origin", "https://cswsh.com") +// .header("Connection", "Upgrade") +// .header("Upgrade", "websocket") +// .method(Method::GET) +// .finish(); + +// assert!(strict_csrf.start(&req).is_err()); +// assert!(lax_csrf.start(&req).is_ok()); +// } +// } diff --git a/src/models.rs b/src/models.rs index ece7ea8..fdc876b 100644 --- a/src/models.rs +++ b/src/models.rs @@ -1,24 +1,4 @@ -use serde::{Deserialize, Serialize}; - -use crate::schema::users; -use yarte::Template; - -#[derive(Debug, Clone, Serialize, Queryable, Identifiable, Deserialize)] -pub struct User { - pub id: i32, - pub name: String, -} - -#[derive(Debug, Clone, Serialize, Insertable, Deserialize)] -#[table_name = "users"] -pub struct NewUser { - pub name: String, -} - -#[derive(Template)] -#[template(path = "hello.hbs")] -pub struct CardTemplate<'a> { - pub title: &'a str, - pub body: String, - pub num: u32, -} +pub mod users; +pub use self::users::*; +pub mod errors; +pub use self::errors::*; diff --git a/src/models/errors.rs b/src/models/errors.rs new file mode 100644 index 0000000..1bf5f77 --- /dev/null +++ b/src/models/errors.rs @@ -0,0 +1,13 @@ +use serde::Serialize; + +#[derive(Debug, Clone, Serialize)] +pub struct JsonErrorModel { + status_code: i16, + pub line: String, + pub reason: String, +} +#[derive(Debug, Clone, Serialize)] +pub struct ErrorModel { + pub status_code: i16, + pub reason: String, +} diff --git a/src/models/roles.rs b/src/models/roles.rs new file mode 100644 index 0000000..e69de29 diff --git a/src/models/users.rs b/src/models/users.rs new file mode 100644 index 0000000..9c22e32 --- /dev/null +++ b/src/models/users.rs @@ -0,0 +1,38 @@ +use serde::{Deserialize, Serialize}; + +use crate::schema::users; +use crate::utils::regexs; +use chrono; +use validator::Validate; +use validator_derive::*; +use yarte::Template; + +#[derive(Debug, Clone, Serialize, Queryable, Identifiable, Deserialize, PartialEq)] +pub struct User { + pub id: i32, + pub name: String, + pub password: String, + pub created_at: chrono::NaiveDateTime, +} + +#[derive(Debug, Clone, Serialize, Insertable, Deserialize, Validate)] +#[table_name = "users"] +pub struct NewUser { + #[validate(regex = "regexs::USERNAME_REG", length(min = 4, max = 10))] + pub name: String, + pub password: String, +} + +#[derive(Debug, Clone, Serialize, Deserialize, Queryable)] +pub struct UserDTO { + pub name: String, + pub registration_date: chrono::NaiveDateTime, +} + +#[derive(Template)] +#[template(path = "hello.hbs")] +pub struct CardTemplate<'a> { + pub title: &'a str, + pub body: String, + pub num: u32, +} diff --git a/src/routes.rs b/src/routes.rs index b374349..4ec67ce 100644 --- a/src/routes.rs +++ b/src/routes.rs @@ -1,2 +1,4 @@ +pub mod auth; pub mod users; +pub use self::auth::*; pub use self::users::*; diff --git a/src/routes/auth.rs b/src/routes/auth.rs new file mode 100644 index 0000000..cc7dc98 --- /dev/null +++ b/src/routes/auth.rs @@ -0,0 +1,45 @@ +use actix_web_httpauth::extractors::basic::BasicAuth; + +use actix_identity::Identity; +use actix_web::{get, post, Error, HttpResponse}; + +#[get("/login")] +pub async fn login(id: Identity, credentials: BasicAuth) -> Result { + let maybe_identity = id.identity(); + let response = if let Some(identity) = maybe_identity { + HttpResponse::Found() + .header("location", "/") + .content_type("text/plain") + .json(format!("Already logged in as {}", identity)) + } else { + id.remember(credentials.user_id().to_string()); + HttpResponse::Found().header("location", "/").finish() + }; + println!("{}", credentials.user_id()); + println!("{:?}", credentials.password()); + Ok(response) +} + +#[get("/logout")] +pub async fn logout(id: Identity, _credentials: BasicAuth) -> Result { + let maybe_identity = id.identity(); + let response = if let Some(identity) = maybe_identity { + info!("Logging out {user}", user = identity); + id.forget(); + HttpResponse::Found().header("location", "/").finish() + } else { + HttpResponse::Found() + .header("location", "/") + .content_type("text/plain") + .json("Not logged in") + }; + Ok(response) +} + +#[get("/")] +pub async fn index(id: Identity) -> String { + format!( + "Hello {}", + id.identity().unwrap_or_else(|| "Anonymous".to_owned()) + ) +} diff --git a/src/routes/users.rs b/src/routes/users.rs index b12caa6..11c62ec 100644 --- a/src/routes/users.rs +++ b/src/routes/users.rs @@ -1,76 +1,82 @@ -use actix_web::{get, post, web, Error, HttpResponse}; +use actix_web::{get, post, web, HttpResponse}; use crate::actions; use crate::models; use crate::types::DbPool; +use actix_web::error::ResponseError; +use std::rc::Rc; /// Finds user by UID. #[get("/api/authzd/users/get/{user_id}")] pub async fn get_user( pool: web::Data, user_uid: web::Path, -) -> Result { +) -> Result { let user_uid = user_uid.into_inner(); // use web::block to offload blocking Diesel code without blocking server thread - let maybe_user = web::block(move || { - let conn = pool.get().map_err(|e| e.to_string())?; - actions::find_user_by_uid(user_uid.into(), &conn).map_err(|e| e.to_string()) + let res = web::block(move || { + let conn = pool.get()?; + actions::find_user_by_uid(user_uid, &conn) }) .await - .map_err(|e| { - error!("{}", e); - HttpResponse::InternalServerError().finish() - })?; - - if let Some(user) = maybe_user { - Ok(HttpResponse::Ok().json(user)) - } else { - let res = HttpResponse::NotFound().body(format!("No user found with uid: {}", user_uid)); - Ok(res) - } + .and_then(|maybe_user| { + if let Some(user) = maybe_user { + Ok(HttpResponse::Ok().json(user)) + } else { + let res = + HttpResponse::NotFound().body(format!("No user found with uid: {}", user_uid)); + Ok(res) + } + }); + res } #[get("/api/authzd/users/get")] -pub async fn get_all_users(pool: web::Data) -> Result { +pub async fn get_all_users(pool: web::Data) -> Result { // use web::block to offload blocking Diesel code without blocking server thread - let maybe_users = web::block(move || { - let conn = pool.get().map_err(|e| e.to_string())?; - actions::get_all(&conn).map_err(|e| e.to_string()) + let res = web::block(move || { + let conn = pool.get()?; + actions::get_all(&conn) }) .await - .map_err(|e| { - eprintln!("{}", e); - HttpResponse::InternalServerError().finish() - })?; - - if let Some(users) = maybe_users { - Ok(HttpResponse::Ok().json(users)) - } else { - let res = HttpResponse::NotFound().body(format!("No users available")); - Ok(res) - } - // Ok(HttpResponse::Ok().json(users)) + .and_then(|maybe_users| { + debug!("{:?}", maybe_users); + if let Some(users) = maybe_users { + if users.is_empty() { + let res = HttpResponse::Ok().json(models::ErrorModel { + status_code: 200, + reason: "No users available".to_string(), + }); + Ok(res) + } else { + Ok(HttpResponse::Ok().json(users)) + } + } else { + let res = HttpResponse::Ok().json(models::ErrorModel { + status_code: 200, + reason: "No users available".to_string(), + }); + Ok(res) + } + }); + res } /// Inserts new user with name defined in form. -#[post("/api/authzd/users/post")] +#[post("/do_registration")] pub async fn add_user( pool: web::Data, form: web::Json, -) -> Result { +) -> Result { // use web::block to offload blocking Diesel code without blocking server thread let user = web::block(move || { - let conn = pool.get().map_err(|e| e.to_string())?; - actions::insert_new_user(&form, &conn).map_err(|e| e.to_string()) + let conn = pool.get()?; + actions::insert_new_user(Rc::new(form.0), &conn) }) .await - .map(|user| { + .and_then(|user| { debug!("{:?}", user); - Ok(HttpResponse::Ok().json(user)) - }) - .map_err(|e| { - eprintln!("{}", e); - HttpResponse::InternalServerError().finish() - })?; + Ok(HttpResponse::Created().json(user)) + }); user } diff --git a/src/schema.rs b/src/schema.rs index 3502adb..69e74dc 100644 --- a/src/schema.rs +++ b/src/schema.rs @@ -2,5 +2,7 @@ table! { users (id) { id -> Integer, name -> Text, + password -> Text, + created_at -> Timestamp, } } diff --git a/src/utils.rs b/src/utils.rs new file mode 100644 index 0000000..9ad9c8d --- /dev/null +++ b/src/utils.rs @@ -0,0 +1,4 @@ +pub mod auth; +pub mod regexs; +pub use self::auth::*; +pub use self::regexs::*; diff --git a/src/utils/auth.rs b/src/utils/auth.rs new file mode 100644 index 0000000..3e7749b --- /dev/null +++ b/src/utils/auth.rs @@ -0,0 +1,14 @@ +use actix_web_httpauth::extractors::basic::BasicAuth; + +// use actix_identity::Identity; +use actix_web::{dev::ServiceRequest, Error}; + +pub async fn validator( + req: ServiceRequest, + credentials: BasicAuth, +) -> Result { + println!("{}", credentials.user_id()); + println!("{:?}", credentials.password()); + // verify credentials from db + Ok(req) +} diff --git a/src/utils/regexs.rs b/src/utils/regexs.rs new file mode 100644 index 0000000..5412462 --- /dev/null +++ b/src/utils/regexs.rs @@ -0,0 +1,5 @@ +use lazy_static::lazy_static; +use regex::Regex; +lazy_static! { + pub static ref USERNAME_REG: Regex = Regex::new(r"^([a-z\d]+-)*[a-z\d]+$").unwrap(); +} diff --git a/test.db b/test.db index b47316914f457f5da9df340c9978139a697b1c49..b4695f70340a2d9b83fcae2081a3981c21453a13 100644 GIT binary patch delta 317 zcmZorXi%6S&B!`Y##xw^LC^jMFY^xuW+qVvrkl)*nM5}>E@tFvbYx~17Z+!2N}Sxt zWXq-C=O3cr7wY4qFnJ%7q_z&1fkNHV!7HL)Z$B|foa zaw)U!=IczljBJd|w-}gjZ5HG>#XK>9%|L~bS<~3az{tSB)WFEl(A31p9K_Q#Fx54% zP%t#KGBvU?G38=lU|?i^$H4p!qH%(hj3BcnC#pVPu%aal%u6;4vdm(RiDG195N~8P zE-g+iGE^~2QZY1AadwRic5yCEv#@kcbTvyf@-E9tEcOpfcMD4iboC9WOg9VDv&^V0 eG1WK9H8ct<3obX;D}_7AQo+E?%Fx`($P56B=TFW6 delta 266 zcmZorXi%6S&B#7c##xx1LHFYhUgjSR%uE#wOgEVqGgWLBWMO0CjAv#S7Z+!2l$|Wk zWGkt|rJ#_Pn478)<{0Gc;TWXg=O3cr7wY4)`8ty>BL@@n76#^9%v*q3S1>cFFipP0 zE@fk+U}$J%YHDR*q{7IoX>4R*Xl`I=WCF%UMg~R(x(24Y21W`77FLF)R>r1W3=9lR z%(oes-!b3bEXZ-5*;I&GlM$rQz|_FV(9qPx$ec-#S(6jhBwnC47Uo+F%-@*bFh6F# r1=M