diff --git a/Cargo.lock b/Cargo.lock index 61881398ee44d31b94580aef26bf877a8b22e407..14f438db6a9a22bccba7aca373539dca447603e3 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1,5 +1,15 @@ # This file is automatically @generated by Cargo. # It is not intended for manual editing. +[[package]] +name = "Inflector" +version = "0.11.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fe438c63458706e03479442743baae6c88256498e6431708f6dfc520a26515d3" +dependencies = [ + "lazy_static", + "regex", +] + [[package]] name = "actix-codec" version = "0.3.0" @@ -31,8 +41,8 @@ dependencies = [ "futures-util", "http", "log", - "rustls", - "tokio-rustls", + "rustls 0.18.1", + "tokio-rustls 0.14.1", "trust-dns-proto", "trust-dns-resolver", "webpki", @@ -96,14 +106,14 @@ dependencies = [ "mime", "percent-encoding", "pin-project 1.0.8", - "rand", + "rand 0.7.3", "regex", "serde", "serde_json", "serde_urlencoded", "sha-1", "slab", - "time", + "time 0.2.27", ] [[package]] @@ -213,10 +223,10 @@ dependencies = [ "actix-service", "actix-utils", "futures-util", - "rustls", - "tokio-rustls", + "rustls 0.18.1", + "tokio-rustls 0.14.1", "webpki", - "webpki-roots", + "webpki-roots 0.20.0", ] [[package]] @@ -269,12 +279,12 @@ dependencies = [ "mime", "pin-project 1.0.8", "regex", - "rustls", + "rustls 0.18.1", "serde", "serde_json", "serde_urlencoded", "socket2", - "time", + "time 0.2.27", "tinyvec", "url", ] @@ -301,12 +311,33 @@ dependencies = [ "futures", ] +[[package]] +name = "addr2line" +version = "0.15.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7a2e47a1fbe209ee101dd6d61285226744c6c8d3c21c8dc878ba6cb9f467f3a" +dependencies = [ + "gimli", +] + [[package]] name = "adler" version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe" +[[package]] +name = "aes" +version = "0.7.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e8b47f52ea9bae42228d07ec09eb676433d7c4ed1ebdf0f1d1c29ed446f1ab8" +dependencies = [ + "cfg-if 1.0.0", + "cipher", + "cpufeatures", + "opaque-debug", +] + [[package]] name = "aho-corasick" version = "0.7.15" @@ -362,19 +393,40 @@ dependencies = [ "log", "mime", "percent-encoding", - "rand", - "rustls", + "rand 0.7.3", + "rustls 0.18.1", "serde", "serde_json", "serde_urlencoded", ] +[[package]] +name = "backtrace" +version = "0.3.59" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4717cfcbfaa661a0fd48f8453951837ae7e8f81e481fbb136e3202d72805a744" +dependencies = [ + "addr2line", + "cc", + "cfg-if 1.0.0", + "libc", + "miniz_oxide", + "object", + "rustc-demangle", +] + [[package]] name = "base-x" version = "0.2.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a4521f3e3d031370679b3b140beb36dfe4801b09ac77e30c61941f97df3ef28b" +[[package]] +name = "base64" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b41b7ea54a0c9d92199de89e20e58d49f02f8e699814ef3fdf266f6f748d15c7" + [[package]] name = "base64" version = "0.12.3" @@ -399,9 +451,26 @@ version = "0.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4152116fd6e9dadb291ae18fc1ec3575ed6d84c29642d97890f4b4a3417297e4" dependencies = [ + "block-padding", "generic-array", ] +[[package]] +name = "block-modes" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2cb03d1bed155d89dce0f845b7899b18a9a163e148fd004e1c28421a783e2d8e" +dependencies = [ + "block-padding", + "cipher", +] + +[[package]] +name = "block-padding" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8d696c370c750c948ada61c69a0ee2cbbb9c50b1019ddb86d9317157a99c2cae" + [[package]] name = "brotli-sys" version = "0.3.2" @@ -422,6 +491,23 @@ dependencies = [ "libc", ] +[[package]] +name = "bson" +version = "1.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "de0aa578035b938855a710ba58d43cfb4d435f3619f99236fb35922a574d6cb1" +dependencies = [ + "base64 0.13.0", + "chrono", + "hex", + "lazy_static", + "linked-hash-map", + "rand 0.7.3", + "serde", + "serde_json", + "uuid", +] + [[package]] name = "buf-min" version = "0.4.0" @@ -482,6 +568,28 @@ version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" +[[package]] +name = "chrono" +version = "0.4.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "670ad68c9088c2a963aaa298cb369688cf3f9465ce5e2d4ca10e6e0098a1ce73" +dependencies = [ + "libc", + "num-integer", + "num-traits", + "time 0.1.43", + "winapi 0.3.9", +] + +[[package]] +name = "cipher" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7ee52072ec15386f770805afd189a01c8841be8696bed250fa2f13c4c0d6dfb7" +dependencies = [ + "generic-array", +] + [[package]] name = "const_fn" version = "0.4.8" @@ -501,7 +609,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "03a5d7b21829bc7b4bf4754a978a241ae54ea55a40f92bb20216e54096f4b951" dependencies = [ "percent-encoding", - "time", + "time 0.2.27", "version_check 0.9.3", ] @@ -520,6 +628,15 @@ dependencies = [ "libc", ] +[[package]] +name = "crc-any" +version = "2.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "073375684a58dece169afbdc9879a027f3698118ad3814938316c6002b7aa921" +dependencies = [ + "debug-helper", +] + [[package]] name = "crc32fast" version = "1.2.1" @@ -529,6 +646,103 @@ dependencies = [ "cfg-if 1.0.0", ] +[[package]] +name = "crypto-mac" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b1d1a86f49236c215f271d40892d5fc950490551400b02ef360692c29815c714" +dependencies = [ + "generic-array", + "subtle", +] + +[[package]] +name = "darling" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0d706e75d87e35569db781a9b5e2416cff1236a47ed380831f959382ccd5f858" +dependencies = [ + "darling_core 0.10.2", + "darling_macro 0.10.2", +] + +[[package]] +name = "darling" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "757c0ded2af11d8e739c4daea1ac623dd1624b06c844cf3f5a39f1bdbd99bb12" +dependencies = [ + "darling_core 0.13.0", + "darling_macro 0.13.0", +] + +[[package]] +name = "darling_core" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0c960ae2da4de88a91b2d920c2a7233b400bc33cb28453a2987822d8392519b" +dependencies = [ + "fnv", + "ident_case", + "proc-macro2", + "quote", + "strsim 0.9.3", + "syn", +] + +[[package]] +name = "darling_core" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2c34d8efb62d0c2d7f60ece80f75e5c63c1588ba68032740494b0b9a996466e3" +dependencies = [ + "fnv", + "ident_case", + "proc-macro2", + "quote", + "strsim 0.10.0", + "syn", +] + +[[package]] +name = "darling_macro" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9b5a2f4ac4969822c62224815d069952656cadc7084fdca9751e6d959189b72" +dependencies = [ + "darling_core 0.10.2", + "quote", + "syn", +] + +[[package]] +name = "darling_macro" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ade7bff147130fe5e6d39f089c6bd49ec0250f35d70b2eebf72afdfc919f15cc" +dependencies = [ + "darling_core 0.13.0", + "quote", + "syn", +] + +[[package]] +name = "debug-helper" +version = "0.3.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "76fbd10dce159c002b9c688ae8ab7cd531151e185e0ad360f4bfea3b0eede3a8" + +[[package]] +name = "derivative" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fcc3dd5e9e9c0b295d6e1e4d811fb6f157d5ffd784b8d202fc62eac8035a770b" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "derive_more" version = "0.99.16" @@ -542,6 +756,17 @@ dependencies = [ "syn", ] +[[package]] +name = "des" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac41dd49fb554432020d52c875fc290e110113f864c6b1b525cd62c7e7747a5d" +dependencies = [ + "byteorder", + "cipher", + "opaque-debug", +] + [[package]] name = "digest" version = "0.9.0" @@ -603,6 +828,20 @@ dependencies = [ "termcolor", ] +[[package]] +name = "err-derive" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "22deed3a8124cff5fa835713fa105621e43bbdc46690c3a6b68328a012d350d4" +dependencies = [ + "proc-macro-error", + "proc-macro2", + "quote", + "rustversion", + "syn", + "synstructure", +] + [[package]] name = "flate2" version = "1.0.22" @@ -768,9 +1007,26 @@ checksum = "8fc3cb4d91f53b50155bdcfd23f6a4c39ae1969c2ae85982b135750cccaf5fce" dependencies = [ "cfg-if 1.0.0", "libc", - "wasi", + "wasi 0.9.0+wasi-snapshot-preview1", +] + +[[package]] +name = "getrandom" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7fcd999463524c52659517fe2cea98493cfe485d10565e7b0fb07dbba7ad2753" +dependencies = [ + "cfg-if 1.0.0", + "libc", + "wasi 0.10.2+wasi-snapshot-preview1", ] +[[package]] +name = "gimli" +version = "0.24.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0e4075386626662786ddb0ec9081e7c7eeb1ba31951f447ca780ef9f5d568189" + [[package]] name = "h2" version = "0.2.7" @@ -815,6 +1071,22 @@ dependencies = [ "libc", ] +[[package]] +name = "hex" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" + +[[package]] +name = "hmac" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2a2a2320eb7ec0ebe8da8f744d7812d9fc4cb4d09344ac01898dbcb6a20ae69b" +dependencies = [ + "crypto-mac", + "digest", +] + [[package]] name = "hostname" version = "0.3.1" @@ -837,18 +1109,80 @@ dependencies = [ "itoa", ] +[[package]] +name = "http-body" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13d5ff830006f7646652e057693569bfe0d51760c0085a071769d142a205111b" +dependencies = [ + "bytes 0.5.6", + "http", +] + [[package]] name = "httparse" version = "1.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "acd94fdbe1d4ff688b67b04eee2e17bd50995534a61539e45adfefb45e5e5503" +[[package]] +name = "httpdate" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "494b4d60369511e7dea41cf646832512a94e542f68bb9c49e54518e0f468eb47" + [[package]] name = "humantime" version = "2.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9a3a5bfb195931eeb336b2a7b4d761daec841b97f947d34394601737a7bba5e4" +[[package]] +name = "hyper" +version = "0.13.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a6f157065790a3ed2f88679250419b5cdd96e714a0d65f7797fd337186e96bb" +dependencies = [ + "bytes 0.5.6", + "futures-channel", + "futures-core", + "futures-util", + "h2", + "http", + "http-body", + "httparse", + "httpdate", + "itoa", + "pin-project 1.0.8", + "socket2", + "tokio", + "tower-service", + "tracing", + "want", +] + +[[package]] +name = "hyper-rustls" +version = "0.21.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "37743cc83e8ee85eacfce90f2f4102030d9ff0a95244098d781e9bee4a90abb6" +dependencies = [ + "bytes 0.5.6", + "futures-util", + "hyper", + "log", + "rustls 0.18.1", + "tokio", + "tokio-rustls 0.14.1", + "webpki", +] + +[[package]] +name = "ident_case" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39" + [[package]] name = "idna" version = "0.2.3" @@ -897,9 +1231,15 @@ dependencies = [ "socket2", "widestring", "winapi 0.3.9", - "winreg", + "winreg 0.6.2", ] +[[package]] +name = "ipnet" +version = "2.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68f2d64f2edebec4ce84ad108148e67e1064789bee435edc5b60ad398714a3a9" + [[package]] name = "itoa" version = "0.4.8" @@ -932,9 +1272,17 @@ dependencies = [ "actix-files", "actix-web", "actix-web-middleware-redirect-https", + "chrono", "dotenv", "env_logger", - "rustls", + "futures", + "magic-crypt", + "rand 0.8.4", + "rustls 0.18.1", + "serde", + "serde_json", + "tokio", + "wither", ] [[package]] @@ -988,6 +1336,23 @@ dependencies = [ "linked-hash-map", ] +[[package]] +name = "magic-crypt" +version = "3.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0f89f3c9a23ba052e4fc602770944c7ef16096ade1ca110a5c722efb16da7395" +dependencies = [ + "aes", + "base64 0.13.0", + "block-modes", + "crc-any", + "des", + "digest", + "md-5", + "sha2", + "tiger", +] + [[package]] name = "match_cfg" version = "0.1.0" @@ -1000,6 +1365,17 @@ version = "0.1.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a3e378b66a060d48947b590737b30a1be76706c8dd7b8ba0f2fe3989c68a853f" +[[package]] +name = "md-5" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7b5a279bb9607f9f53c22d496eade00d138d1bdcccd07d74650387cf94942a15" +dependencies = [ + "block-buffer", + "digest", + "opaque-debug", +] + [[package]] name = "memchr" version = "2.3.4" @@ -1045,12 +1421,24 @@ dependencies = [ "kernel32-sys", "libc", "log", - "miow", + "miow 0.2.2", "net2", "slab", "winapi 0.2.8", ] +[[package]] +name = "mio-named-pipes" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0840c1c50fd55e521b247f949c241c9997709f23bd7f023b9762cd561e935656" +dependencies = [ + "log", + "mio", + "miow 0.3.7", + "winapi 0.3.9", +] + [[package]] name = "mio-uds" version = "0.6.8" @@ -1075,12 +1463,66 @@ dependencies = [ ] [[package]] -name = "net2" -version = "0.2.37" +name = "miow" +version = "0.3.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "391630d12b68002ae1e25e8f974306474966550ad82dac6886fb8910c19568ae" +checksum = "b9f1c5b025cda876f66ef43a113f91ebc9f4ccef34843000e0adf6ebbab84e21" dependencies = [ - "cfg-if 0.1.10", + "winapi 0.3.9", +] + +[[package]] +name = "mongodb" +version = "1.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dd2a1cb6fd58c27e51ee650dca3b6924c4ce533dc0384f05b3219709ea1f1eb6" +dependencies = [ + "async-trait", + "base64 0.11.0", + "bitflags", + "bson", + "chrono", + "derivative", + "err-derive", + "futures", + "hex", + "hmac", + "lazy_static", + "md-5", + "os_info", + "pbkdf2", + "percent-encoding", + "rand 0.7.3", + "reqwest", + "rustls 0.17.0", + "serde", + "serde_bytes", + "serde_with", + "sha-1", + "sha2", + "socket2", + "stringprep", + "strsim 0.10.0", + "take_mut", + "time 0.1.43", + "tokio", + "tokio-rustls 0.13.1", + "trust-dns-proto", + "trust-dns-resolver", + "typed-builder", + "uuid", + "version_check 0.9.3", + "webpki", + "webpki-roots 0.21.1", +] + +[[package]] +name = "net2" +version = "0.2.37" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "391630d12b68002ae1e25e8f974306474966550ad82dac6886fb8910c19568ae" +dependencies = [ + "cfg-if 0.1.10", "libc", "winapi 0.3.9", ] @@ -1095,6 +1537,25 @@ dependencies = [ "version_check 0.1.5", ] +[[package]] +name = "num-integer" +version = "0.1.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d2cc698a63b549a70bc047073d2949cce27cd1c7b0a4a862d08a8031bc2801db" +dependencies = [ + "autocfg", + "num-traits", +] + +[[package]] +name = "num-traits" +version = "0.2.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a64b1ec5cda2586e284722486d802acf1f7dbdc623e2bfc57e65ca1cd099290" +dependencies = [ + "autocfg", +] + [[package]] name = "num_cpus" version = "1.13.0" @@ -1105,6 +1566,12 @@ dependencies = [ "libc", ] +[[package]] +name = "object" +version = "0.24.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a5b3dd1c072ee7963717671d1ca129f1048fda25edea6b752bfc71ac8854170" + [[package]] name = "once_cell" version = "1.8.0" @@ -1117,6 +1584,16 @@ version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "624a8340c38c1b80fd549087862da4ba43e08858af025b236e509b6649fc13d5" +[[package]] +name = "os_info" +version = "3.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ac91020bfed8cc3f8aa450d4c3b5fa1d3373fc091c8a92009f3b27749d5a227" +dependencies = [ + "log", + "winapi 0.3.9", +] + [[package]] name = "parking_lot" version = "0.11.2" @@ -1142,6 +1619,15 @@ dependencies = [ "winapi 0.3.9", ] +[[package]] +name = "pbkdf2" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d95f5254224e617595d2cc3cc73ff0a5eaf2637519e25f03388154e9378b6ffa" +dependencies = [ + "crypto-mac", +] + [[package]] name = "percent-encoding" version = "2.1.0" @@ -1221,6 +1707,30 @@ version = "0.2.10" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ac74c624d6b2d21f425f752262f42188365d7b8ff1aff74c82e45136510a4857" +[[package]] +name = "proc-macro-error" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da25490ff9892aab3fcf7c36f08cfb902dd3e71ca0f9f9517bea02a73a5ce38c" +dependencies = [ + "proc-macro-error-attr", + "proc-macro2", + "quote", + "syn", + "version_check 0.9.3", +] + +[[package]] +name = "proc-macro-error-attr" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1be40180e52ecc98ad80b184934baf3d0d29f979574e439af5a55274b35f869" +dependencies = [ + "proc-macro2", + "quote", + "version_check 0.9.3", +] + [[package]] name = "proc-macro-hack" version = "0.5.19" @@ -1263,11 +1773,24 @@ version = "0.7.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6a6b1679d49b24bbfe0c803429aa1874472f50d9b363131f0e89fc356b544d03" dependencies = [ - "getrandom", + "getrandom 0.1.16", + "libc", + "rand_chacha 0.2.2", + "rand_core 0.5.1", + "rand_hc 0.2.0", + "rand_pcg", +] + +[[package]] +name = "rand" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2e7573632e6454cf6b99d7aac4ccca54be06da05aca2ef7423d22d27d4d4bcd8" +dependencies = [ "libc", - "rand_chacha", - "rand_core", - "rand_hc", + "rand_chacha 0.3.1", + "rand_core 0.6.3", + "rand_hc 0.3.1", ] [[package]] @@ -1277,7 +1800,17 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f4c8ed856279c9737206bf725bf36935d8666ead7aa69b52be55af369d193402" dependencies = [ "ppv-lite86", - "rand_core", + "rand_core 0.5.1", +] + +[[package]] +name = "rand_chacha" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" +dependencies = [ + "ppv-lite86", + "rand_core 0.6.3", ] [[package]] @@ -1286,7 +1819,16 @@ version = "0.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "90bde5296fc891b0cef12a6d03ddccc162ce7b2aff54160af9338f8d40df6d19" dependencies = [ - "getrandom", + "getrandom 0.1.16", +] + +[[package]] +name = "rand_core" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d34f1408f55294453790c48b2f1ebbb1c5b4b7563eb1f418bcfcfdbb06ebb4e7" +dependencies = [ + "getrandom 0.2.3", ] [[package]] @@ -1295,7 +1837,25 @@ version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ca3129af7b92a17112d59ad498c6f81eaf463253766b90396d39ea7a39d6613c" dependencies = [ - "rand_core", + "rand_core 0.5.1", +] + +[[package]] +name = "rand_hc" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d51e9f596de227fda2ea6c84607f5558e196eeaf43c986b724ba4fb8fdf497e7" +dependencies = [ + "rand_core 0.6.3", +] + +[[package]] +name = "rand_pcg" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "16abd0c1b639e9eb4d7c50c0b8100b0d0f849be2349829c740fe8e6eb4816429" +dependencies = [ + "rand_core 0.5.1", ] [[package]] @@ -1324,6 +1884,43 @@ version = "0.6.25" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f497285884f3fcff424ffc933e56d7cbca511def0c9831a7f9b5f6153e3cc89b" +[[package]] +name = "reqwest" +version = "0.10.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0718f81a8e14c4dbb3b34cf23dc6aaf9ab8a0dfec160c534b3dbca1aaa21f47c" +dependencies = [ + "base64 0.13.0", + "bytes 0.5.6", + "encoding_rs", + "futures-core", + "futures-util", + "http", + "http-body", + "hyper", + "hyper-rustls", + "ipnet", + "js-sys", + "lazy_static", + "log", + "mime", + "mime_guess", + "percent-encoding", + "pin-project-lite 0.2.7", + "rustls 0.18.1", + "serde", + "serde_json", + "serde_urlencoded", + "tokio", + "tokio-rustls 0.14.1", + "url", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", + "webpki-roots 0.20.0", + "winreg 0.7.0", +] + [[package]] name = "resolv-conf" version = "0.7.0" @@ -1349,6 +1946,12 @@ dependencies = [ "winapi 0.3.9", ] +[[package]] +name = "rustc-demangle" +version = "0.1.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7ef03e0a2b150c7a90d01faf6254c9c48a41e95fb2a8c2ac1c6f0d2b9aefc342" + [[package]] name = "rustc_version" version = "0.2.3" @@ -1367,6 +1970,19 @@ dependencies = [ "semver 0.11.0", ] +[[package]] +name = "rustls" +version = "0.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c0d4a31f5d68413404705d6982529b0e11a9aacd4839d1d6222ee3b8cb4015e1" +dependencies = [ + "base64 0.11.0", + "log", + "ring", + "sct", + "webpki", +] + [[package]] name = "rustls" version = "0.18.1" @@ -1380,6 +1996,12 @@ dependencies = [ "webpki", ] +[[package]] +name = "rustversion" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "61b3909d758bb75c79f23d4736fac9433868679d3ad2ea7a61e3c25cfda9a088" + [[package]] name = "ryu" version = "1.0.5" @@ -1444,6 +2066,15 @@ dependencies = [ "serde_derive", ] +[[package]] +name = "serde_bytes" +version = "0.11.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "16ae07dd2f88a366f15bd0632ba725227018c69a1c8550a927324f8eb8368bb9" +dependencies = [ + "serde", +] + [[package]] name = "serde_derive" version = "1.0.130" @@ -1461,6 +2092,7 @@ version = "1.0.68" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0f690853975602e1bfe1ccbf50504d67174e3bcf340f23b5ea9992e0587a52d8" dependencies = [ + "indexmap", "itoa", "ryu", "serde", @@ -1478,6 +2110,29 @@ dependencies = [ "serde", ] +[[package]] +name = "serde_with" +version = "1.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ad6056b4cb69b6e43e3a0f055def223380baecc99da683884f205bf347f7c4b3" +dependencies = [ + "rustversion", + "serde", + "serde_with_macros", +] + +[[package]] +name = "serde_with_macros" +version = "1.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "12e47be9471c72889ebafb5e14d5ff930d89ae7a67bbdb5f8abb564f845a927e" +dependencies = [ + "darling 0.13.0", + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "sha-1" version = "0.9.8" @@ -1497,6 +2152,19 @@ version = "0.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2579985fda508104f7587689507983eadd6a6e84dd35d6d115361f530916fa0d" +[[package]] +name = "sha2" +version = "0.9.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b69f9a4c9740d74c5baa3fd2e547f9525fa8088a8a958e0ca2409a514e33f5fa" +dependencies = [ + "block-buffer", + "cfg-if 1.0.0", + "cpufeatures", + "digest", + "opaque-debug", +] + [[package]] name = "signal-hook-registry" version = "1.4.0" @@ -1593,6 +2261,34 @@ version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "213701ba3370744dcd1a12960caa4843b3d68b4d1c0a5d575e0d65b2ee9d16c0" +[[package]] +name = "stringprep" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ee348cb74b87454fff4b551cbf727025810a004f88aeacae7f85b87f4e9a1c1" +dependencies = [ + "unicode-bidi", + "unicode-normalization", +] + +[[package]] +name = "strsim" +version = "0.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6446ced80d6c486436db5c078dde11a9f73d42b57fb273121e160b84f63d894c" + +[[package]] +name = "strsim" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623" + +[[package]] +name = "subtle" +version = "2.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6bdef32e8150c2a081110b42772ffe7d7c9032b606bc226c8260fd97e0976601" + [[package]] name = "syn" version = "1.0.76" @@ -1604,6 +2300,24 @@ dependencies = [ "unicode-xid", ] +[[package]] +name = "synstructure" +version = "0.12.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f36bdaa60a83aca3921b5259d5400cbf5e90fc51931376a9bd4a0eb79aa7210f" +dependencies = [ + "proc-macro2", + "quote", + "syn", + "unicode-xid", +] + +[[package]] +name = "take_mut" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f764005d11ee5f36500a149ace24e00e3da98b0158b3e2d53a7495660d3f4d60" + [[package]] name = "termcolor" version = "1.1.2" @@ -1642,6 +2356,27 @@ dependencies = [ "num_cpus", ] +[[package]] +name = "tiger" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "443e531cbcf9de83258cfef70bcd56c91188de5819ebd4b19c85f589e0617005" +dependencies = [ + "block-buffer", + "byteorder", + "digest", +] + +[[package]] +name = "time" +version = "0.1.43" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ca8a50ef2360fbd1eeb0ecd46795a87a19024eb4b53c5dc916ca1fd95fe62438" +dependencies = [ + "libc", + "winapi 0.3.9", +] + [[package]] name = "time" version = "0.2.27" @@ -1702,19 +2437,46 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6703a273949a90131b290be1fe7b039d0fc884aa1935860dfcbe056f28cd8092" dependencies = [ "bytes 0.5.6", + "fnv", "futures-core", "iovec", "lazy_static", "libc", "memchr", "mio", + "mio-named-pipes", "mio-uds", + "num_cpus", "pin-project-lite 0.1.12", "signal-hook-registry", "slab", + "tokio-macros", "winapi 0.3.9", ] +[[package]] +name = "tokio-macros" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e44da00bfc73a25f814cd8d7e57a68a5c31b74b3152a0a1d1f590c97ed06265a" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "tokio-rustls" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "15cb62a0d2770787abc96e99c1cd98fcf17f94959f3af63ca85bdfb203f051b4" +dependencies = [ + "futures-core", + "rustls 0.17.0", + "tokio", + "webpki", +] + [[package]] name = "tokio-rustls" version = "0.14.1" @@ -1722,7 +2484,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e12831b255bcfa39dc0436b01e19fea231a37db570686c06ee72c423479f889a" dependencies = [ "futures-core", - "rustls", + "rustls 0.18.1", "tokio", "webpki", ] @@ -1741,6 +2503,12 @@ dependencies = [ "tokio", ] +[[package]] +name = "tower-service" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "360dfd1d6d30e05fda32ace2c8c70e9c0a9da713275777f5a4dbb8a1893930c6" + [[package]] name = "tracing" version = "0.1.28" @@ -1779,13 +2547,14 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1cad71a0c0d68ab9941d2fb6e82f8fb2e86d9945b94e1661dd0aaea2b88215a9" dependencies = [ "async-trait", + "backtrace", "cfg-if 1.0.0", "enum-as-inner", "futures", "idna", "lazy_static", "log", - "rand", + "rand 0.7.3", "smallvec", "thiserror", "tokio", @@ -1811,6 +2580,23 @@ dependencies = [ "trust-dns-proto", ] +[[package]] +name = "try-lock" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "59547bce71d9c38b83d9c0e92b6066c4253371f15005def0c30d9657f50c7642" + +[[package]] +name = "typed-builder" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dfc955f27acc7a547f328f52f4a5a568986a31efec2fc6de865279f3995787b9" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "typenum" version = "1.14.0" @@ -1877,6 +2663,15 @@ dependencies = [ "percent-encoding", ] +[[package]] +name = "uuid" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bc5cf98d8186244414c848017f0e2676b3fcb46807f6668a97dfe67359a3c4b7" +dependencies = [ + "getrandom 0.2.3", +] + [[package]] name = "v_escape" version = "0.15.0" @@ -1921,12 +2716,28 @@ version = "0.9.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5fecdca9a5291cc2b8dcf7dc02453fee791a280f3743cb0905f8822ae463b3fe" +[[package]] +name = "want" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ce8a968cb1cd110d136ff8b819a556d6fb6d919363c61534f6860c7eb172ba0" +dependencies = [ + "log", + "try-lock", +] + [[package]] name = "wasi" version = "0.9.0+wasi-snapshot-preview1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cccddf32554fecc6acb585f82a32a72e28b48f8c4c1883ddfeeeaa96f7d8e519" +[[package]] +name = "wasi" +version = "0.10.2+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fd6fbd9a79829dd1ad0cc20627bf1ed606756a7f77edff7b66b7064f9cb327c6" + [[package]] name = "wasm-bindgen" version = "0.2.78" @@ -1934,6 +2745,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "632f73e236b219150ea279196e54e610f5dbafa5d61786303d4da54f84e47fce" dependencies = [ "cfg-if 1.0.0", + "serde", + "serde_json", "wasm-bindgen-macro", ] @@ -1952,6 +2765,18 @@ dependencies = [ "wasm-bindgen-shared", ] +[[package]] +name = "wasm-bindgen-futures" +version = "0.4.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e8d7523cb1f2a4c96c1317ca690031b714a51cc14e05f712446691f413f5d39" +dependencies = [ + "cfg-if 1.0.0", + "js-sys", + "wasm-bindgen", + "web-sys", +] + [[package]] name = "wasm-bindgen-macro" version = "0.2.78" @@ -2010,6 +2835,15 @@ dependencies = [ "webpki", ] +[[package]] +name = "webpki-roots" +version = "0.21.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aabe153544e473b775453675851ecc86863d2a81d786d741f6b76778f2a48940" +dependencies = [ + "webpki", +] + [[package]] name = "widestring" version = "0.4.3" @@ -2068,6 +2902,47 @@ dependencies = [ "winapi 0.3.9", ] +[[package]] +name = "winreg" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0120db82e8a1e0b9fb3345a539c478767c0048d842860994d96113d5b667bd69" +dependencies = [ + "winapi 0.3.9", +] + +[[package]] +name = "wither" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "45e6fce5f641da433789cf4376cd698874666bf2741eb4d72444b4006cc0954a" +dependencies = [ + "async-trait", + "chrono", + "futures", + "log", + "mongodb", + "serde", + "thiserror", + "wither_derive", +] + +[[package]] +name = "wither_derive" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c7cc57cffdfd2239926b5b9e068591944444c71c8e9baa2bd1cbde3492d78973" +dependencies = [ + "Inflector", + "async-trait", + "darling 0.10.2", + "proc-macro-error", + "proc-macro2", + "quote", + "serde", + "syn", +] + [[package]] name = "ws2_32-sys" version = "0.2.1" diff --git a/Cargo.toml b/Cargo.toml index 2f390d40cc86e14a3e61aab05d42e13997b5cbde..b8a874e5fe4b15c0bbb40d5ba89902411d1770a5 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -12,5 +12,13 @@ actix-web = { version = "3", features=["rustls"] } actix-web-middleware-redirect-https = "3.0.1" rustls="0.18.1" actix-files="0.5" +futures = "0.3.17" +serde = "1" +serde_json="1" +wither="0.9" +magic-crypt="3" env_logger="0.9" +chrono="0.4" +rand="0.8" dotenv="0.15" +tokio = { version = "0.2", features = ["full"] } \ No newline at end of file diff --git a/dev.docker-compose.yml b/dev.docker-compose.yml index 0552a29e35b9facd676617c5dc451b7ae7433248..bc1bd9724d27d18a7bd42b25e01c36635c56fd0a 100644 --- a/dev.docker-compose.yml +++ b/dev.docker-compose.yml @@ -5,6 +5,8 @@ services: context: . dockerfile: ./dev.Dockerfile container_name: "kuadrado_server" + depends_on: + - ${DATABASE_NAME} restart: unless-stopped ports: - 80:${SERVER_PORT} @@ -17,3 +19,17 @@ services: command: cargo run env_file: - ./.env + kuadradodb: + build: ./mongo/ + container_name: ${DATABASE_NAME} + environment: + - MONGO_INITDB_DATABASE=${DATABASE_NAME} + - MONGO_INITDB_ROOT_USERNAME=${DB_ROOT_USERNAME} + - MONGO_INITDB_ROOT_PASSWORD=${DB_ROOT_PASSWORD} + - MONGO_INITDB_NON_ROOT_USERNAME=${DB_USERNAME} + - MONGO_INITDB_NON_ROOT_PASSWORD=${DB_USER_PASSWORD} + volumes: + - ./mongo/init-mongo.js:/docker-entrypoint-initdb.d/init-mongo.js:ro + - /var/${DATABASE_NAME}-volume:/data/db + ports: + - "27017-27019:27017-27019" diff --git a/docker-compose.yml b/docker-compose.yml index d65e02efb0ff4d1c6443ec06b82e76caee85e484..35cef6e09d669b7b328b8afc4ee37f9b9d572f8f 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -5,6 +5,8 @@ services: context: . dockerfile: ./Dockerfile container_name: "kuadrado_server" + depends_on: + - ${DATABASE_NAME} restart: unless-stopped ports: - 80:${SERVER_PORT} @@ -14,3 +16,17 @@ services: - /etc/letsencrypt/:${RESOURCES_DIR}/certs:ro env_file: - ./.env + kuadradodb: + build: ./mongo/ + container_name: ${DATABASE_NAME} + environment: + - MONGO_INITDB_DATABASE=${DATABASE_NAME} + - MONGO_INITDB_ROOT_USERNAME=${DB_ROOT_USERNAME} + - MONGO_INITDB_ROOT_PASSWORD=${DB_ROOT_PASSWORD} + - MONGO_INITDB_NON_ROOT_USERNAME=${DB_USERNAME} + - MONGO_INITDB_NON_ROOT_PASSWORD=${DB_USER_PASSWORD} + volumes: + - ./mongo/init-mongo.js:/docker-entrypoint-initdb.d/init-mongo.js:ro + - /var/${DATABASE_NAME}-volume:/data/db + ports: + - "27017-27019:27017-27019" diff --git a/mongo/Dockerfile b/mongo/Dockerfile new file mode 100644 index 0000000000000000000000000000000000000000..1623104f669b51bcdbfda018f9ba40dcb39c1471 --- /dev/null +++ b/mongo/Dockerfile @@ -0,0 +1,2 @@ +from mongo +RUN mkdir /mongoinit && chown mongodb -R /mongoinit && chgrp mongodb -R /mongoinit \ No newline at end of file diff --git a/mongo/init-mongo.js b/mongo/init-mongo.js new file mode 100644 index 0000000000000000000000000000000000000000..52495ec801115591c325a1fb66688ec64ce99199 --- /dev/null +++ b/mongo/init-mongo.js @@ -0,0 +1,33 @@ +function getEnvVariable(envVar) { + // Thanks for the tip: https://dev.to/jsheridanwells/dockerizing-a-mongo-database-4jf2 + + const command = run("sh", "-c", `printenv --null ${envVar} >/mongoinit/${envVar}.txt`); + // note: 'printenv --null' prevents adding line break to value + + if (command != 0) return Error("Failed to retrieve env variable : " + envVar); + + // .replace(/\0/g, '') removes the NULL characters + return cat(`/mongoinit/${envVar}.txt`).replace(/\0/g, ''); +} + +db.createUser({ + user: getEnvVariable("MONGO_INITDB_NON_ROOT_USERNAME"), + pwd: getEnvVariable("MONGO_INITDB_NON_ROOT_PASSWORD"), + roles: [ + { + role: "readWrite", + db: getEnvVariable("MONGO_INITDB_DATABASE") + } + ] +}); + +db = new Mongo().getDB(getEnvVariable("MONGO_INITDB_DATABASE")); + +db.createCollection("articles"); +db.createCollection("administrators"); + +db.administrators.createIndex({ "username": 1 }, { unique: true }); +db.administrators.createIndex({ "auth_token": 1 }, { unique: true }); + + +run("sh", "-c", "rm -rf /mongoinit/*"); \ No newline at end of file diff --git a/public/views/404/404.html b/public/views/404/404.html new file mode 100644 index 0000000000000000000000000000000000000000..435c37509f8eeec80b791649c27a33caebca3c5e --- /dev/null +++ b/public/views/404/404.html @@ -0,0 +1,13 @@ +<!DOCTYPE html> +<html> + +<head> + <meta charset="utf-8"> + <title>Page not found</title> +</head> + +<body> + <h1>404 : Page not found</h1> +</body> + +</html> \ No newline at end of file diff --git a/public/views/admin-login/assets/login.js b/public/views/admin-login/assets/login.js new file mode 100644 index 0000000000000000000000000000000000000000..aee791408d27aad0cb9475a11d6755cc8b12f9ab --- /dev/null +++ b/public/views/admin-login/assets/login.js @@ -0,0 +1,16 @@ +const login_form = document.getElementById("login-form"); +login_form.onsubmit = e => { + e.preventDefault(); + fetch("/admin-auth", { + method: "POST", + body: new URLSearchParams(new FormData(e.target)) + }).then(res => { + if (res.status >= 400) { + alert(res.statusText) + } else { + window.location.assign("/v/admin-panel") + } + }).catch(err => { + alert(err.statusText || "Server error") + }); +} \ No newline at end of file diff --git a/public/views/admin-login/index.html b/public/views/admin-login/index.html new file mode 100644 index 0000000000000000000000000000000000000000..ce8dfbf824d5682b33b3e1f454f28dc55b6caad2 --- /dev/null +++ b/public/views/admin-login/index.html @@ -0,0 +1,21 @@ +<!DOCTYPE html> +<html> + +<head> + <meta charset="utf-8"> + <title>Kuadrado admin login</title> +</head> + +<body> + <h1>Admin login</h1> + <form id="login-form"> + <label for="username"></label> + <input type="text" id="username" name="username"> + <label for="password"></label> + <input type="password" id="password" name="password"> + <input type="submit"> + </form> +</body> +<script src="/v/admin-login/assets/login.js"></script> + +</html> \ No newline at end of file diff --git a/public/views/admin-panel/assets/script.js b/public/views/admin-panel/assets/script.js new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/public/views/admin-panel/index.html b/public/views/admin-panel/index.html new file mode 100644 index 0000000000000000000000000000000000000000..b9fc1fea3ed2f7165f1fc31c26001cda206df7fa --- /dev/null +++ b/public/views/admin-panel/index.html @@ -0,0 +1,21 @@ +<!DOCTYPE html> +<html> + +<head> + <meta charset="utf-8"> + <title>Kuadrado admin panel</title> + <link rel="shortcut icon" type="image/x-icon" href="/v/admin-panel/assets/favicon.ico" /> + <link rel="stylesheet" href="/v/admin-panel/assets/style.css"> +</head> + +<body> + <main> + Kuadrado admin panel TODO + FORM: post article + FORM: update/delete article + </main> +</body> + +<script src="/v/admin-panel/assets/script.js"></script> + +</html> \ No newline at end of file diff --git a/public/views/test-view-auth/index.html b/public/views/test-view-auth/index.html new file mode 100644 index 0000000000000000000000000000000000000000..91c451a43d074893c7a157b6d2e13aea1c79fec2 --- /dev/null +++ b/public/views/test-view-auth/index.html @@ -0,0 +1 @@ +<h1>TEST AUTH</h1> \ No newline at end of file diff --git a/public/views/test-view/index.html b/public/views/test-view/index.html new file mode 100644 index 0000000000000000000000000000000000000000..6e4c7653e016d0d4cbb6b93fd020a37242de4202 --- /dev/null +++ b/public/views/test-view/index.html @@ -0,0 +1 @@ +<h1>TEST</h1> \ No newline at end of file diff --git a/public/views/unauthorized/unauthorized.html b/public/views/unauthorized/unauthorized.html new file mode 100644 index 0000000000000000000000000000000000000000..af9827a5bc04bff7fb0c3cbb5a753a7ecfd59508 --- /dev/null +++ b/public/views/unauthorized/unauthorized.html @@ -0,0 +1,15 @@ +<!DOCTYPE html> +<html> + +<head> + <meta charset="utf-8"> + <title>Unauthorized</title> +</head> + +<body> + <h1>Unauthorized</h1> + <p>You must login as an administrator to access this page</p> + <a href='/v/admin-login'>Login page</a> +</body> + +</html> \ No newline at end of file diff --git a/src/app_state.rs b/src/app_state.rs new file mode 100644 index 0000000000000000000000000000000000000000..c1744971b663d64b95f1f77909f114b77b3b4f25 --- /dev/null +++ b/src/app_state.rs @@ -0,0 +1,66 @@ +use crate::{crypto::Encryption, env::Env, init_admin::create_default_admin_if_none}; +use wither::mongodb::{options::ClientOptions, Client, Database}; + +#[derive(Debug, Clone)] +/// The app_state that must be given to the actix::App instance using App::new().app_data(web::Data::new(AppState::new())) +/// It holds the database client connection, an Env struct which provides all values defined in the .env file, +/// and an Encryption struct which is reponsible for encrypting and decrypting data such as passwords and auth tokens. +pub struct AppState { + pub db: Database, + pub env: Env, + pub encryption: Encryption, +} + +impl AppState { + /// Creates the Mongodb database client connection + async fn get_db_connection(host: &str, env: &Env) -> Database { + let db_connection_string = format!( + "mongodb://{}:{}@{}:{}/{}", + env.db_username, env.db_user_pwd, host, env.db_port, env.db_name + ); + + let client_options = ClientOptions::parse(&db_connection_string) + .await + .expect("Error creating database client options"); + + let client = Client::with_options(client_options).expect("Couldn't connect to database."); + client.database(&env.db_name) + } + + pub async fn new() -> Self { + let env = Env::new(); + let db = Self::get_db_connection(&env.db_name, &env).await; + + let encryption = Encryption::new(env.crypt_key.to_owned()); + + AppState { + db, + env, + encryption, + } + } + + /// This calls Self::new() and creates a default administrator before returning the instance + pub async fn with_default_admin_user() -> Self { + let instance = Self::new().await; + if let Err(e) = create_default_admin_if_none(&instance).await { + panic!("Error creating admin user: {}\nWill exit process now.", e); + }; + instance + } + + #[cfg(test)] + /// Provides an instance with some specificities for testing + pub async fn for_test() -> Self { + let env = Env::for_test(); + let db = Self::get_db_connection("localhost", &env).await; + + let encryption = Encryption::new(env.crypt_key.to_owned()); + + AppState { + db, + env, + encryption, + } + } +} diff --git a/src/crypto.rs b/src/crypto.rs new file mode 100644 index 0000000000000000000000000000000000000000..fb02f6af89913bb0685ddd41222af196ebeeaadb --- /dev/null +++ b/src/crypto.rs @@ -0,0 +1,91 @@ +use magic_crypt::{MagicCrypt, MagicCryptTrait, SecureBit}; +use rand::{distributions::Alphanumeric, Rng}; + +#[derive(Debug, Clone)] +/// A structure responsible of encrypting and decrypting data such as auth token, passwords and email addresses. +pub struct Encryption { + /// The encryption key must be keeped secret and is loaded from the $CRYPT_KEY environment variable + pub key: String, +} + +impl Encryption { + pub fn new(key: String) -> Self { + Encryption { key } + } + + /// Gets a string as an argument and returns a base64 hash of the string based on the secret key and magic_crypt::SecureBit::Bit256 algorithm + pub fn encrypt(&self, source: &String) -> String { + let mc = MagicCrypt::new(&self.key, SecureBit::Bit256, None::<String>); + mc.encrypt_str_to_base64(source) + } + + /// Gets a string base64 hash as an argument and returns the decryted string. + /// Panics if the source base64 string cannot be decrypted (should happen if trying to decrypt a regular string) + #[cfg(test)] + pub fn decrypt(&self, source: &String) -> String { + let mc = MagicCrypt::new(&self.key, SecureBit::Bit256, None::<String>); + mc.decrypt_base64_to_string(source).unwrap() + } + + /// Generates a random ascii lowercase string. Length being given as argument. + pub fn random_ascii_lc_string(&self, length: usize) -> String { + // Thanks to https://stackoverflow.com/questions/54275459/how-do-i-create-a-random-string-by-sampling-from-alphanumeric-characters#54277357 + rand::thread_rng() + .sample_iter(&Alphanumeric) + .take(length) + .map(char::from) + .collect::<String>() + .to_ascii_lowercase() + } +} + +/*@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@*@@ + *@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@*@@ + * _______ ______ ______ _______ *@@ + * |__ __@ | ____@ / ____@ |__ __@ *@@ + * | @ | @__ \_ @_ | @ *@@ + * | @ | __@ \ @_ | @ *@@ + * | @ | @___ ____\ @ | @ *@@ + * |__@ |______@ \______@ |__@ *@@ + * *@@ + *@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@*@@ + *@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@*@*/ + +#[cfg(test)] +mod test_encryption { + use super::*; + + #[test] + fn test_random_ascii_lc_string() { + dotenv::dotenv().ok(); + let key = std::env::var("CRYPT_KEY").unwrap(); + let enc = Encryption::new(key); + let rdm_str = enc.random_ascii_lc_string(32); + assert_eq!(rdm_str.len(), 32); + assert!(rdm_str.chars().all(char::is_alphanumeric)); + assert_eq!(rdm_str, rdm_str.to_lowercase()); + } + + #[test] + fn test_encrypt() { + dotenv::dotenv().ok(); + let key = std::env::var("CRYPT_KEY").unwrap(); + let enc = Encryption::new(key); + + let an_email = String::from("kuadrado-email@test.com"); + let email_hash = enc.encrypt(&an_email); + assert_ne!(an_email, email_hash); + } + + #[test] + fn test_decrypt() { + dotenv::dotenv().ok(); + let key = std::env::var("CRYPT_KEY").unwrap(); + let enc = Encryption::new(key); + + let an_email = String::from("kuadrado-email@test.com"); + let email_hash = enc.encrypt(&an_email); + let decrypted = enc.decrypt(&email_hash); + assert_eq!(an_email, decrypted); + } +} diff --git a/src/env.rs b/src/env.rs index e1ab149816238b5dec64c2015e023d341fbd31f2..cf5860ba3b3edac6a6ce52757cd4b1000b0c336a 100644 --- a/src/env.rs +++ b/src/env.rs @@ -1,5 +1,19 @@ use std::env; +#[derive(Debug, Clone)] +/// Makes a copy of all required values defined in the system environment variables +pub struct Env { + pub release_mode: String, + pub db_username: String, + pub db_user_pwd: String, + pub db_name: String, + pub db_port: String, + pub server_host: String, + pub crypt_key: String, + pub default_admin_username: String, + pub default_admin_password: String, +} + static RELEASE_MODES: [&str; 3] = ["debug", "test", "prod"]; pub fn get_release_mode() -> String { @@ -25,3 +39,39 @@ pub fn get_log_level() -> String { _ => String::from("info"), } } + +impl Env { + pub fn new() -> Env { + Env { + release_mode: get_release_mode(), + db_username: env::var("DB_USERNAME").expect("DB_USERNAME is not defined."), + db_user_pwd: env::var("DB_USER_PASSWORD").expect("DB_USER_PASSWORD is not defined."), + db_name: env::var("DATABASE_NAME").expect("DATABASE_NAME is not defined."), + db_port: env::var("DB_PORT").expect("DB_PORT is not defined."), + server_host: env::var("SERVER_HOST").expect("SERVER_HOST is not defined"), + crypt_key: env::var("CRYPT_KEY").expect("CRYPT_KEY is not defined."), + default_admin_username: env::var("DEFAULT_ADMIN_USERNAME") + .expect("DEFAULT_ADMIN_USERNAME is not defined"), + default_admin_password: env::var("DEFAULT_ADMIN_PASSWORD") + .expect("DEFAULT_ADMIN_PASSWORD is not defined"), + } + } + + #[cfg(test)] + /// Returns an instance with some values adjusted for testing such as email addresses + pub fn for_test() -> Env { + Env { + release_mode: String::from("debug"), + db_username: env::var("DB_USERNAME").expect("DB_USERNAME is not defined."), + db_user_pwd: env::var("DB_USER_PASSWORD").expect("DB_USER_PASSWORD is not defined."), + db_name: env::var("DATABASE_NAME").expect("DATABASE_NAME is not defined."), + db_port: env::var("DB_PORT").expect("DB_PORT is not defined."), + server_host: env::var("SERVER_HOST").expect("SERVER_HOST is not defined"), + crypt_key: env::var("CRYPT_KEY").expect("CRYPT_KEY is not defined."), + default_admin_username: env::var("DEFAULT_ADMIN_USERNAME") + .expect("DEFAULT_ADMIN_USERNAME is not defined"), + default_admin_password: env::var("DEFAULT_ADMIN_PASSWORD") + .expect("DEFAULT_ADMIN_PASSWORD is not defined"), + } + } +} diff --git a/src/init_admin.rs b/src/init_admin.rs new file mode 100644 index 0000000000000000000000000000000000000000..dbe1f2906689770d962d15b74a2ea8c4a6cbc927 --- /dev/null +++ b/src/init_admin.rs @@ -0,0 +1,35 @@ +use crate::model::Administrator; +use crate::AppState; +use wither::{bson::doc, prelude::Model}; + +/// Creates the default administrator if it doesn't already exists and returns a Result. +pub async fn create_default_admin_if_none(app_state: &AppState) -> Result<(), String> { + let admin_username = app_state.env.default_admin_username.to_owned(); + let admin_password = app_state.env.default_admin_password.to_owned(); + + let admin = Administrator::from_values(app_state, admin_username, admin_password); + + let admin_doc = doc! { + "username": &admin.username, + "password_hash": &admin.password_hash + }; + + match Administrator::find_one(&app_state.db, admin_doc, None).await { + Ok(found_user) => match found_user { + Some(_) => Ok(()), + None => { + println!("Kuadrado admin will be created"); + match app_state + .db + .collection_with_type::<Administrator>("administrators") + .insert_one(admin, None) + .await + { + Ok(_) => Ok(()), + Err(e) => Err(format!("Error creating administrator: {:?}", e)), + } + } + }, + Err(e) => Err(format!("Error creating administrator: {:?}", e)), + } +} diff --git a/src/main.rs b/src/main.rs index fa51ed907681b9ee70647924e5f625a97ba26284..4b3b905d78a291a9c426ad782798a2c722aa3046 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,19 +1,32 @@ -//! # REST API server for the Mentalo application +//! # WEB SERVER FOR THE KUADRADO SOFTWARE WEBSITE +mod app_state; +mod crypto; mod env; +mod init_admin; +mod middleware; +mod model; +mod service; mod standard_static_files; mod tls; +mod view; +mod view_resource; use actix_files::Files; use actix_web::{ - middleware::Logger, - web::{get, resource, to}, + middleware::{normalize::TrailingSlash, Logger, NormalizePath}, + web::{get, resource, scope, to, Data}, App, HttpResponse, HttpServer, }; use actix_web_middleware_redirect_https::RedirectHTTPS; +use app_state::AppState; use env::get_log_level; use env_logger::Env; +use middleware::AuthenticatedAdminMiddleware; +use service::admin_auth::admin_auth; use standard_static_files::{favicon, robots, sitemap}; use std::env::var as env_var; use tls::get_tls_config; +use view::get_view; +use view_resource::{ViewResourceDescriptor, ViewResourceManager}; #[actix_web::main] async fn main() -> std::io::Result<()> { @@ -25,6 +38,8 @@ async fn main() -> std::io::Result<()> { std::path::PathBuf::from(env_var("RESOURCES_DIR").expect("RESOURCES_DIR is not defined")) .join("public"); + let app_state = AppState::with_default_admin_user().await; + HttpServer::new(move || { App::new() .wrap(Logger::default()) @@ -33,7 +48,38 @@ async fn main() -> std::io::Result<()> { format!(":{}", server_port), format!(":{}", server_port_tls), )])) - // .wrap(NormalizePath::new(TrailingSlash::Trim)) + .app_data(Data::new(app_state.clone())) + .app_data(Data::new(AuthenticatedAdminMiddleware::new( + "kuadrado-admin-auth", + ))) + .app_data(Data::new(ViewResourceManager::with_views(vec![ + ViewResourceDescriptor { + path_str: "admin-panel", + index_file_name: "index.html", + resource_name: "admin-panel", + apply_auth_middleware: true, + }, + ViewResourceDescriptor { + path_str: "admin-login", + index_file_name: "index.html", + resource_name: "admin-login", + apply_auth_middleware: false, + }, + ViewResourceDescriptor { + path_str: "404", + index_file_name: "404.html", + resource_name: "404", + apply_auth_middleware: false, + }, + ViewResourceDescriptor { + path_str: "unauthorized", + index_file_name: "unauthorized.html", + resource_name: "unauthorized", + apply_auth_middleware: false, + }, + ]))) + .wrap(NormalizePath::new(TrailingSlash::Trim)) + .service(admin_auth) // Allow json payload to have size until ~32MB // .app_data(JsonConfig::default().limit(1 << 25u8)) ///////////////////////////////////////////////////////////////////////////////////////////////////////////// @@ -42,6 +88,21 @@ async fn main() -> std::io::Result<()> { .service(resource("/robots.txt").route(get().to(robots))) .service(resource("/sitemap.xml").route(get().to(sitemap))) ///////////////////////////////////////////////////////////////////////////////////////////////////////////// + // VIEWS //////////////////////////////////////////////////////////////////////////////////////////////////// + .service( + scope("/v") + .service(Files::new( + "/admin-panel/assets", + public_dir.join("views/admin-panel/assets"), + )) + .service(Files::new( + "/admin-login/assets", + public_dir.join("views/admin-login/assets"), + )) + // get_view will match any url to we put it at last + .service(get_view), + ) + ///////////////////////////////////////////////////////////////////////////////////////////////////////////// // PUBLIC WEBSITE ////////////////////////////////////////////////////////////////////////////////////////////// .service(Files::new("/", &public_dir).index_file("index.html")) ///////////////////////////////////////////////////////////////////////////////////////////////////////////// diff --git a/src/middleware.rs b/src/middleware.rs new file mode 100644 index 0000000000000000000000000000000000000000..a239355e41c5c75adadd7f8a4fc89aed3c2789b3 --- /dev/null +++ b/src/middleware.rs @@ -0,0 +1,120 @@ +use crate::{ + model::{AdminAuthCredentials, Administrator}, + AppState, +}; +use actix_web::{http::Cookie, web::Form, HttpMessage, HttpRequest}; +use wither::{bson::doc, prelude::Model}; + +/// Returns a actix_web::http::Cookie instance designed to be an authentication cookie. +pub fn get_auth_cookie(name: &'static str, value: String) -> Cookie<'static> { + Cookie::build(name, value) + .secure(true) + .http_only(true) + .path("/") + .finish() +} + +/// This is not a real middleware as it is meant to be executed only after having processed the request and not before. +/// It must be registered in the actix App instance with app_data. +/// ``` +/// App::new() +/// .app_data(Data::new(AuthenticatedAdminMiddleware::new("some-auth-cookie-name"))) +/// ``` +/// If a service need to perform an authentication before doing anything, this "pseudo-middleware" should be run before anything else in the function. +/// Example: +/// ``` +/// #[post("/some-url")] +/// pub async fn some_service( +/// app_state: Data<AppState>, +/// middleware: Data<AuthenticatedAdminMiddleware<'_>>, +/// req: HttpRequest, +/// ) -> impl Responder { +/// if middleware.exec(&app_state, &req, None).await.is_err() { +/// return HttpResponse::Unauthorized().finish(); +/// } +/// ... Authenticated action .... +/// } +/// ``` +#[derive(Debug, Clone)] +pub struct AuthenticatedAdminMiddleware<'a> { + /// The name of the authentication cookie + pub cookie_name: &'a str, +} + +impl<'a> AuthenticatedAdminMiddleware<'a> { + pub fn new(cookie_name: &'a str) -> Self { + AuthenticatedAdminMiddleware { cookie_name } + } + + /// Performs Administrator authentication from form data with username and password + /// Returns an authentication Cookie instance if the authentication succeeds, or an error. + async fn try_auth_from_form_data( + &self, + app_state: &AppState, + form_data: Form<AdminAuthCredentials>, + ) -> Result<Cookie<'static>, ()> { + match Administrator::authenticated(app_state, form_data.into_inner()).await { + Ok(ref mut admin) => { + let auth_token = app_state.encryption.random_ascii_lc_string(256); + admin.auth_token = Some(app_state.encryption.encrypt(&auth_token)); + + if admin + .save(&app_state.db, Some(doc!("_id": admin.id().unwrap()))) + .await + .is_err() + { + println!("Failed to update admin auth_token"); + return Err(()); + } + + let cookie = get_auth_cookie("kuadrado-admin-auth", auth_token.to_owned()); + + return Ok(cookie); + } + Err(_) => return Err(()), + } + } + + /// Performs Administrator authentication from the authentication cookie value + async fn try_auth_from_auth_cookie( + &self, + app_state: &AppState, + cookie: &Cookie<'static>, + ) -> Result<Cookie<'static>, ()> { + match Administrator::authenticated_with_cookie(app_state, &cookie).await { + Ok(_) => return Ok(cookie.clone()), + Err(_) => return Err(()), + } + } + + /// The function that must be called in order to execute the verification. + /// Example : + /// ``` + /// #[post("/some-url")] + /// pub async fn some_service( + /// app_state: Data<AppState>, + /// middleware: Data<AuthenticatedAdminMiddleware<'_>>, + /// req: HttpRequest, + /// ) -> impl Responder { + /// if middleware.exec(&app_state, &req, None).await.is_err() { + /// return HttpResponse::Unauthorized().finish(); + /// } + /// ... Authenticated actions .... + /// } + /// ``` + pub async fn exec( + &self, + app_state: &AppState, + req: &HttpRequest, + auth_form_data: Option<Form<AdminAuthCredentials>>, + ) -> Result<Cookie<'static>, ()> { + let auth_cookie = req.cookie(self.cookie_name); + if let Some(form_data) = auth_form_data { + return self.try_auth_from_form_data(app_state, form_data).await; + } else if let Some(cookie) = auth_cookie { + return self.try_auth_from_auth_cookie(app_state, &cookie).await; + } else { + return Err(()); + } + } +} diff --git a/src/model.rs b/src/model.rs new file mode 100644 index 0000000000000000000000000000000000000000..b1c6c6fc9a9c137ad3577d3e30d87c7de36ca1be --- /dev/null +++ b/src/model.rs @@ -0,0 +1,2 @@ +mod administrator; +pub use administrator::*; diff --git a/src/model/administrator.rs b/src/model/administrator.rs new file mode 100644 index 0000000000000000000000000000000000000000..852881ee77b737185433d500a7b1b8229076914a --- /dev/null +++ b/src/model/administrator.rs @@ -0,0 +1,83 @@ +use crate::AppState; +use actix_web::http::Cookie; +use serde::{Deserialize, Serialize}; +use wither::{ + bson::{doc, oid::ObjectId}, + prelude::Model, +}; + +#[derive(Debug, Serialize, Deserialize)] +/// The data type that must sent by form data POST to authenticate an administrator. +pub struct AdminAuthCredentials { + pub username: String, + pub password: String, +} + +#[derive(Debug, Deserialize, Serialize, Model)] +#[model(index( + keys = r#"doc!{"email": 1, "username": 1}"#, + options = r#"doc!{"unique": true}"# +))] +/// An administrator is a user with registered authentication credentials access right to the admin-panel and has ability to perform admin actions such as gam review, moderation, etc. +pub struct Administrator { + #[serde(rename = "_id", skip_serializing_if = "Option::is_none")] + pub id: Option<ObjectId>, + pub username: String, + pub password_hash: String, + pub auth_token: Option<String>, +} + +impl Administrator { + /// Creates an administrator with values for username and password. + /// The auth_token fields remains None as it must be created if the user authenticates itself with the provided credentials + /// The password is stored as password_hash, it is encrypted with the AppState::Encryption. + pub fn from_values(app_state: &AppState, username: String, password: String) -> Self { + Administrator { + id: None, + password_hash: app_state.encryption.encrypt(&password), + username, + auth_token: None, + } + } + + /// Performs authentication with form data <username, password>. + /// Returns a Result with either an authenticated Administrator instance or an error. + pub async fn authenticated( + app_state: &AppState, + credentials: AdminAuthCredentials, + ) -> Result<Self, ()> { + let filter_doc = doc! { + "password_hash": app_state.encryption.encrypt(&credentials.password), + "username": credentials.username + }; + + match Administrator::find_one(&app_state.db, filter_doc, None).await { + Ok(user_option) => match user_option { + Some(admin) => Ok(admin), + None => Err(()), + }, + Err(_) => Err(()), + } + } + + /// Performs authenticattion with auth cookie. The cookie value must match the Administrator auth_token value. + /// Returns a result with either the authenticated admin, or an empty Err. + pub async fn authenticated_with_cookie( + app_state: &AppState, + auth_cookie: &Cookie<'_>, + ) -> Result<Self, ()> { + let cookie_value = auth_cookie.value().to_string(); + + let filter_doc = doc! { + "auth_token": app_state.encryption.encrypt(&cookie_value), + }; + + match Administrator::find_one(&app_state.db, filter_doc, None).await { + Ok(user_option) => match user_option { + Some(admin) => Ok(admin), + None => Err(()), + }, + Err(_) => Err(()), + } + } +} diff --git a/src/service.rs b/src/service.rs new file mode 100644 index 0000000000000000000000000000000000000000..ee031a2bfed8a87c699691c425cf5c996db73240 --- /dev/null +++ b/src/service.rs @@ -0,0 +1 @@ +pub mod admin_auth; diff --git a/src/service/admin_auth.rs b/src/service/admin_auth.rs new file mode 100644 index 0000000000000000000000000000000000000000..989c22fe9a2af59a6cddad38789e092b31ba8019 --- /dev/null +++ b/src/service/admin_auth.rs @@ -0,0 +1,122 @@ +use crate::{middleware::AuthenticatedAdminMiddleware, model::AdminAuthCredentials, AppState}; +use actix_web::{ + post, + web::{Data, Form}, + HttpMessage, HttpRequest, HttpResponse, Responder, +}; + +/// Performs administrator authentication from form data +/// If the authentication succeed, a cookie with an auth token is returned +/// If not, 401 is returned and if an auth cookie is found it is deleted. +#[post("/admin-auth")] +pub async fn admin_auth<'a>( + app_state: Data<AppState>, + auth_mw: Data<AuthenticatedAdminMiddleware<'a>>, + req: HttpRequest, + form_data: Form<AdminAuthCredentials>, +) -> impl Responder { + let cookie_opt = auth_mw.exec(&app_state, &req, Some(form_data)).await; + match cookie_opt { + Ok(cookie) => HttpResponse::Accepted().cookie(cookie).finish(), + Err(_) => { + return match req.cookie(auth_mw.cookie_name) { + Some(c) => { + // Invalidate auth_cookie if auth failed in any way + HttpResponse::Unauthorized().del_cookie(&c).finish() + } + None => HttpResponse::Unauthorized().finish(), + }; + } + } +} + +/*@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@*@@ + *@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@*@@ + * _______ ______ ______ _______ *@@ + * |__ __@ | ____@ / ____@ |__ __@ *@@ + * | @ | @__ \_ @_ | @ *@@ + * | @ | __@ \ @_ | @ *@@ + * | @ | @___ ____\ @ | @ *@@ + * |__@ |______@ \______@ |__@ *@@ + * *@@ + *@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@*@@ + *@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@*@*/ + +#[cfg(test)] +mod test_admin_auth { + use super::*; + use crate::model::Administrator; + use actix_web::{ + http::{Method, StatusCode}, + test, + web::Data, + App, + }; + use futures::stream::StreamExt; + use wither::prelude::Model; + + #[tokio::test] + async fn test_admin_auth() { + dotenv::dotenv().ok(); + + let app_state = AppState::for_test().await; + let admin_user = Administrator::find(&app_state.db, None, None) + .await + .unwrap() + .next() + .await + .unwrap() + .unwrap(); // Get the first admin user we find. At least one should exist. + + let password = app_state.encryption.decrypt(&admin_user.password_hash); + let username = admin_user.username.to_owned(); + + let mut app = test::init_service( + App::new() + .app_data(Data::new(app_state.clone())) + .app_data(Data::new(AuthenticatedAdminMiddleware::new( + "kuadrado-admin-auth", + ))) + .service(admin_auth), + ) + .await; + + let req = test::TestRequest::with_uri("/admin-auth") + .method(Method::POST) + .set_form(&AdminAuthCredentials { username, password }) + .to_request(); + + let resp = test::call_service(&mut app, req).await; + + assert_eq!(resp.status(), StatusCode::ACCEPTED); + } + + #[tokio::test] + async fn test_admin_auth_unauthorized() { + dotenv::dotenv().ok(); + + let app_state = AppState::for_test().await; + + let mut app = test::init_service( + App::new() + .app_data(Data::new(app_state.clone())) + .app_data(Data::new(AuthenticatedAdminMiddleware::new( + "kuadrado-admin-auth", + ))) + .service(admin_auth), + ) + .await; + + let req = test::TestRequest::with_uri("/admin-auth") + .method(Method::POST) + .set_form(&AdminAuthCredentials { + username: String::from("whatever"), + password: String::from("whatever"), + }) + .to_request(); + + let resp = test::call_service(&mut app, req).await; + + assert_eq!(resp.status(), StatusCode::UNAUTHORIZED); + } +} diff --git a/src/view.rs b/src/view.rs new file mode 100644 index 0000000000000000000000000000000000000000..49f05b1507ab12596f6ecf282cd013923e1f10f3 --- /dev/null +++ b/src/view.rs @@ -0,0 +1,191 @@ +use crate::{ + middleware::AuthenticatedAdminMiddleware, view_resource::ViewResourceManager, AppState, +}; +use actix_web::{ + get, + web::{Data, Path}, + HttpRequest, Responder, +}; + +/// Returns the content of a ViewResource after retrieving it by name. +/// If the resource is not found (has not been registered), it returns the 404 ViewResource. +/// The regex matches uris with more than 3 characters so we don't match the /fr /es etc urls used for the websites translation directories +#[get("/{resource_name:.{3,}}")] +pub async fn get_view<'a>( + app_state: Data<AppState>, + resource_manager: Data<ViewResourceManager>, + auth_middleware: Data<AuthenticatedAdminMiddleware<'a>>, + req: HttpRequest, + resource_name: Path<String>, +) -> impl Responder { + resource_manager + .get_resource_as_http_response( + &app_state, + &auth_middleware, + &req, + None, + &resource_name.into_inner(), + ) + .await +} + +/*@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@*@@ + *@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@*@@ + * _______ ______ ______ _______ *@@ + * |__ __@ | ____@ / ____@ |__ __@ *@@ + * | @ | @__ \_ @_ | @ *@@ + * | @ | __@ \ @_ | @ *@@ + * | @ | @___ ____\ @ | @ *@@ + * |__@ |______@ \______@ |__@ *@@ + * *@@ + *@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@*@@ + *@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@*@*/ + +#[cfg(test)] +mod test_views { + use super::*; + use crate::{ + middleware::get_auth_cookie, + model::{AdminAuthCredentials, Administrator}, + view_resource::ViewResourceDescriptor, + }; + use actix_web::{ + http::StatusCode, + test, + web::{Bytes, Data}, + App, + }; + + fn get_views_manager() -> ViewResourceManager { + ViewResourceManager::with_views(vec![ + ViewResourceDescriptor { + path_str: "test-view", + index_file_name: "index.html", + resource_name: "test-view", + apply_auth_middleware: false, + }, + ViewResourceDescriptor { + path_str: "test-view-auth", + index_file_name: "index.html", + resource_name: "test-view-auth", + apply_auth_middleware: true, + }, + ]) + } + + async fn get_authenticated_admin(app_state: &AppState) -> Administrator { + Administrator::authenticated( + app_state, + AdminAuthCredentials { + username: app_state.env.default_admin_username.to_owned(), + password: app_state.env.default_admin_password.to_owned(), + }, + ) + .await + .unwrap() + } + + #[tokio::test] + async fn test_get_view() { + dotenv::dotenv().ok(); + + let app_state = AppState::for_test().await; + + let mut app = test::init_service( + App::new() + .app_data(Data::new(app_state.clone())) + .app_data(Data::new(AuthenticatedAdminMiddleware::new( + "kuadrado-admin-auth", + ))) + .app_data(Data::new(get_views_manager())) + .service(get_view), + ) + .await; + + let req = test::TestRequest::with_uri("/test-view").to_request(); + let resp = test::call_service(&mut app, req).await; + assert_eq!(resp.status(), StatusCode::OK); + + let body = test::read_body(resp).await; + assert_eq!(body, Bytes::from("<h1>TEST</h1>")); + } + + #[tokio::test] + async fn test_get_view_auth() { + dotenv::dotenv().ok(); + + let app_state = AppState::for_test().await; + + let mut app = test::init_service( + App::new() + .app_data(Data::new(app_state.clone())) + .app_data(Data::new(AuthenticatedAdminMiddleware::new( + "kuadrado-admin-auth", + ))) + .app_data(Data::new(get_views_manager())) + .service(get_view), + ) + .await; + let admin_user = get_authenticated_admin(&app_state).await; + + let req = test::TestRequest::with_uri("/test-view-auth") + .cookie(get_auth_cookie( + "kuadrado-admin-auth", + app_state + .encryption + .decrypt(&admin_user.auth_token.unwrap()) + .to_owned(), + )) + .to_request(); + + let resp = test::call_service(&mut app, req).await; + assert_eq!(resp.status(), StatusCode::OK); + + let body = test::read_body(resp).await; + assert_eq!(body, Bytes::from("<h1>TEST AUTH</h1>")); + } + + #[tokio::test] + async fn test_get_view_auth_unauthorized() { + dotenv::dotenv().ok(); + + let app_state = AppState::for_test().await; + + let mut app = test::init_service( + App::new() + .app_data(Data::new(app_state.clone())) + .app_data(Data::new(AuthenticatedAdminMiddleware::new( + "kuadrado-admin-auth", + ))) + .app_data(Data::new(get_views_manager())) + .service(get_view), + ) + .await; + + let req = test::TestRequest::with_uri("/test-view-auth").to_request(); + let resp = test::call_service(&mut app, req).await; + assert_eq!(resp.status(), StatusCode::UNAUTHORIZED); + } + + #[tokio::test] + async fn test_get_view_not_found() { + dotenv::dotenv().ok(); + + let app_state = AppState::for_test().await; + + let mut app = test::init_service( + App::new() + .app_data(Data::new(app_state.clone())) + .app_data(Data::new(AuthenticatedAdminMiddleware::new( + "kuadrado-admin-auth", + ))) + .app_data(Data::new(get_views_manager())) + .service(get_view), + ) + .await; + + let req = test::TestRequest::with_uri("/whatever").to_request(); + let resp = test::call_service(&mut app, req).await; + assert_eq!(resp.status(), StatusCode::NOT_FOUND); + } +} diff --git a/src/view_resource.rs b/src/view_resource.rs new file mode 100644 index 0000000000000000000000000000000000000000..1a4449032bd7467c0b05548c05d6304ed162a62a --- /dev/null +++ b/src/view_resource.rs @@ -0,0 +1,156 @@ +use crate::{ + middleware::AuthenticatedAdminMiddleware, + model::AdminAuthCredentials, + // view_resource::{ViewResource, ViewResourceDescriptor}, + AppState, +}; +use actix_web::{web::Form, HttpMessage, HttpRequest, HttpResponse}; +use std::{env::var as env_var, fs::read_to_string as file_to_string, path::PathBuf}; + +#[derive(Debug, Clone)] +/// Loads a static resource data allowing it to be served by the get_view service. +/// It holds a name, allowing the resource to be retrived by name, +/// a content which can be any text content stored in a string (like an html document), +/// a path to the directory of the actual static resource, and a boolean which indicates wether +/// or not an authentication verification should be applied. +pub struct ViewResource { + pub name: String, + pub string_contents: String, + pub dir_path: PathBuf, + pub apply_auth_middleware: bool, +} + +#[derive(Debug, Clone)] +/// Defines the values that will be used to construct a ViewResource. +/// It must be passed to the AppViewResourceManager for resource registration +pub struct ViewResourceDescriptor<'a> { + pub path_str: &'a str, + pub index_file_name: &'a str, + pub resource_name: &'a str, + pub apply_auth_middleware: bool, +} + +#[derive(Debug, Clone)] +/// A structure reponsible of registering and retrieving static resources. +pub struct ViewResourceManager { + resources: Vec<ViewResource>, +} + +impl ViewResourceManager { + pub fn new() -> Self { + ViewResourceManager { resources: vec![] } + } + + /// Calls the constructor and registers the resources described as argument before returning the instance + pub fn with_views(resource_descriptors: Vec<ViewResourceDescriptor>) -> Self { + let mut instance = Self::new(); + instance.register_batch(resource_descriptors); + instance + } + + /// Registers a new static resource in the instance. + /// The path provided in the argument must point to an existing file + pub fn register(&mut self, desc: ViewResourceDescriptor) { + let static_dir = std::path::PathBuf::from( + env_var("RESOURCES_DIR").expect("RESOURCES_DIR is not defined"), + ) + .join("public/views"); + + let dir_path = static_dir.join(desc.path_str); + + let path: PathBuf = format!("{}/{}", dir_path.to_str().unwrap(), desc.index_file_name) + .parse() + .expect(&format!( + "Failed to pare resource index file path {:?}", + desc.index_file_name + )); + + let string_contents = file_to_string(path).unwrap(); + + &self.resources.push(ViewResource { + name: desc.resource_name.to_string(), + dir_path, + string_contents, + apply_auth_middleware: desc.apply_auth_middleware, + }); + } + + /// Registers a collection of multiple resources. + pub fn register_batch(&mut self, resource_descriptors: Vec<ViewResourceDescriptor>) { + for desc in resource_descriptors.iter() { + self.register(desc.clone()); + } + } + + /// Retrieves a resource by name and returns a reference to it or None. + pub fn get_resource(&self, name: &str) -> Option<&ViewResource> { + self.resources.iter().find(|res| res.name == name) + } + + /// Retrieves a resource by name and returns it as an http response. + /// This can be returned as it by a service. + pub async fn get_resource_as_http_response<'a>( + &self, + app_state: &AppState, + auth_middleware: &AuthenticatedAdminMiddleware<'a>, + req: &HttpRequest, + auth_data: Option<Form<AdminAuthCredentials>>, + resource_name: &str, + ) -> HttpResponse { + match self.get_resource(resource_name) { + Some(res) => { + if res.apply_auth_middleware { + let auth_cookie = auth_middleware.exec(app_state, req, auth_data).await; + if auth_cookie.is_err() { + let unauthorized_view = match self.get_resource("unauthorized") { + Some(res_404) => res_404.string_contents.to_string(), + None => { + println!("WARNING: missing Unauthorized view resource"); + + " + <h1>Unauthorized</h1> + <p>You must login as an administrator to access this page</p> + <a href='/v/admin-login'>Login page</a> + " + .to_string() + } + }; + + let mut response_builder = HttpResponse::Unauthorized(); + + return match req.cookie(auth_middleware.cookie_name) { + Some(cookie) => { + // Invalidate auth_cookie if auth failed in any way + response_builder + .del_cookie(&cookie) + .content_type("text/html") + .body(unauthorized_view) + } + None => response_builder + .content_type("text/html") + .body(unauthorized_view), + }; + } else { + return HttpResponse::Ok() + .content_type("text/html") + .cookie(auth_cookie.unwrap()) + .body(&res.string_contents); + } + } + + HttpResponse::Ok() + .content_type("text/html") + .body(&res.string_contents) + } + None => match self.get_resource("404") { + Some(res_404) => HttpResponse::NotFound() + .content_type("text/html") + .body(&res_404.string_contents), + None => { + println!("WARNING: missing 404 view resource"); + HttpResponse::NotFound().finish() + } + }, + } + } +}