diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..b47f64d --- /dev/null +++ b/.dockerignore @@ -0,0 +1,3 @@ +/target +.env.local +tarpaulin-report.html diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..330ca44 --- /dev/null +++ b/.gitignore @@ -0,0 +1,6 @@ +/target +.env.local +tarpaulin-report.html +tmp/ +node_modules/ +assets/ diff --git a/Cargo.lock b/Cargo.lock new file mode 100644 index 0000000..e0751a5 --- /dev/null +++ b/Cargo.lock @@ -0,0 +1,3692 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 3 + +[[package]] +name = "actix-auth-middleware" +version = "0.2.0" +source = "git+https://github.com/realaravinth/actix-auth-middleware?branch=v4#81fc0adcb54a7601afe479f8408261f18c8f8d89" +dependencies = [ + "actix-http", + "actix-identity", + "actix-service", + "actix-web", + "futures", +] + +[[package]] +name = "actix-codec" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "57a7559404a7f3573127aab53c08ce37a6c6a315c374a31070f3c91cd1b4a7fe" +dependencies = [ + "bitflags", + "bytes", + "futures-core", + "futures-sink", + "log", + "memchr", + "pin-project-lite", + "tokio", + "tokio-util", +] + +[[package]] +name = "actix-http" +version = "3.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c83abf9903e1f0ad9973cc4f7b9767fd5a03a583f51a5b7a339e07987cd2724" +dependencies = [ + "actix-codec", + "actix-rt", + "actix-service", + "actix-utils", + "ahash", + "base64", + "bitflags", + "brotli", + "bytes", + "bytestring", + "derive_more", + "encoding_rs", + "flate2", + "futures-core", + "h2", + "http", + "httparse", + "httpdate", + "itoa 1.0.4", + "language-tags", + "local-channel", + "mime", + "percent-encoding", + "pin-project-lite", + "rand 0.8.5", + "sha1", + "smallvec", + "tracing", + "zstd", +] + +[[package]] +name = "actix-identity" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "171fe3ed055b2dd50c61967911d253d47e76e1d4308acfbf99fc7affe5ec42aa" +dependencies = [ + "actix-service", + "actix-utils", + "actix-web", + "futures-util", + "serde", + "serde_json", + "time", +] + +[[package]] +name = "actix-macros" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "465a6172cf69b960917811022d8f29bc0b7fa1398bc4f78b3c466673db1213b6" +dependencies = [ + "quote", + "syn", +] + +[[package]] +name = "actix-router" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d66ff4d247d2b160861fa2866457e85706833527840e4133f8f49aa423a38799" +dependencies = [ + "bytestring", + "http", + "regex", + "serde", + "tracing", +] + +[[package]] +name = "actix-rt" +version = "2.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7ea16c295198e958ef31930a6ef37d0fb64e9ca3b6116e6b93a8bdae96ee1000" +dependencies = [ + "actix-macros", + "futures-core", + "tokio", +] + +[[package]] +name = "actix-server" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0da34f8e659ea1b077bb4637948b815cd3768ad5a188fdcd74ff4d84240cd824" +dependencies = [ + "actix-rt", + "actix-service", + "actix-utils", + "futures-core", + "futures-util", + "mio", + "num_cpus", + "socket2", + "tokio", + "tracing", +] + +[[package]] +name = "actix-service" +version = "2.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b894941f818cfdc7ccc4b9e60fa7e53b5042a2e8567270f9147d5591893373a" +dependencies = [ + "futures-core", + "paste", + "pin-project-lite", +] + +[[package]] +name = "actix-utils" +version = "3.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "88a1dcdff1466e3c2488e1cb5c36a71822750ad43839937f85d2f4d9f8b705d8" +dependencies = [ + "local-waker", + "pin-project-lite", +] + +[[package]] +name = "actix-web" +version = "4.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d48f7b6534e06c7bfc72ee91db7917d4af6afe23e7d223b51e68fffbb21e96b9" +dependencies = [ + "actix-codec", + "actix-http", + "actix-macros", + "actix-router", + "actix-rt", + "actix-server", + "actix-service", + "actix-utils", + "actix-web-codegen", + "ahash", + "bytes", + "bytestring", + "cfg-if", + "cookie", + "derive_more", + "encoding_rs", + "futures-core", + "futures-util", + "http", + "itoa 1.0.4", + "language-tags", + "log", + "mime", + "once_cell", + "pin-project-lite", + "regex", + "serde", + "serde_json", + "serde_urlencoded", + "smallvec", + "socket2", + "time", + "url", +] + +[[package]] +name = "actix-web-codegen" +version = "4.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fa9362663c8643d67b2d5eafba49e4cb2c8a053a29ed00a0bea121f17c76b13" +dependencies = [ + "actix-router", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "actix-web-codegen-const-routes" +version = "0.1.0" +source = "git+https://github.com/realaravinth/actix-web-codegen-const-routes?tag=0.1.0#1cc9b8dbaaef4b3634dabbf537f313200dd91bd9" +dependencies = [ + "actix-router", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "adler" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe" + +[[package]] +name = "aead" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c192eb8f11fc081b0fe4259ba5af04217d4e0faddd02417310a927911abd7c8" +dependencies = [ + "crypto-common", + "generic-array", +] + +[[package]] +name = "aes" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "433cfd6710c9986c576a25ca913c39d66a6474107b406f34f91d4a8923395241" +dependencies = [ + "cfg-if", + "cipher", + "cpufeatures", +] + +[[package]] +name = "aes-gcm" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "82e1366e0c69c9f927b1fa5ce2c7bf9eafc8f9268c0b9800729e8b267612447c" +dependencies = [ + "aead", + "aes", + "cipher", + "ctr", + "ghash", + "subtle", +] + +[[package]] +name = "ahash" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fcb51a0695d8f838b1ee009b3fbf66bda078cd64590202a864a8f3e8c4315c47" +dependencies = [ + "getrandom 0.2.8", + "once_cell", + "version_check", +] + +[[package]] +name = "aho-corasick" +version = "0.7.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cc936419f96fa211c1b9166887b38e5e40b19958e5b895be7c1f93adec7071ac" +dependencies = [ + "memchr", +] + +[[package]] +name = "alloc-no-stdlib" +version = "2.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cc7bb162ec39d46ab1ca8c77bf72e890535becd1751bb45f64c597edb4c8c6b3" + +[[package]] +name = "alloc-stdlib" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94fb8275041c72129eb51b7d0322c29b8387a0386127718b096429201a5d6ece" +dependencies = [ + "alloc-no-stdlib", +] + +[[package]] +name = "ammonia" +version = "3.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "64e6d1c7838db705c9b756557ee27c384ce695a1c51a6fe528784cb1c6840170" +dependencies = [ + "html5ever", + "maplit", + "once_cell", + "tendril", + "url", +] + +[[package]] +name = "argon2-creds" +version = "0.2.2" +source = "git+https://github.com/realaravinth/argon2-creds?branch=master#9f43fd564448cae609d148a700de91e2aea6474c" +dependencies = [ + "ammonia", + "derive_builder", + "derive_more", + "lazy_static", + "rand 0.8.5", + "regex", + "rust-argon2", + "unicode-normalization", + "validator", +] + +[[package]] +name = "arrayref" +version = "0.3.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4c527152e37cf757a3f78aae5a06fbeefdb07ccc535c980a3208ee3060dd544" + +[[package]] +name = "arrayvec" +version = "0.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8da52d66c7071e2e3fa2a1e5c6d088fec47b593032b254f5e980de8ea54454d6" + +[[package]] +name = "async-compression" +version = "0.3.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "942c7cd7ae39e91bde4820d74132e9862e62c2f386c3aa90ccf55949f5bad63a" +dependencies = [ + "brotli", + "flate2", + "futures-core", + "memchr", + "pin-project-lite", + "tokio", +] + +[[package]] +name = "async-trait" +version = "0.1.59" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "31e6e93155431f3931513b243d371981bb2770112b370c82745a1d19d2f99364" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "atoi" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d7c57d12312ff59c811c0643f4d80830505833c9ffaebd193d819392b265be8e" +dependencies = [ + "num-traits", +] + +[[package]] +name = "atty" +version = "0.2.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9b39be18770d11421cdb1b9947a45dd3f37e93092cbf377614828a319d5fee8" +dependencies = [ + "hermit-abi", + "libc", + "winapi", +] + +[[package]] +name = "autocfg" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa" + +[[package]] +name = "base64" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e1b586273c5702936fe7b7d6896644d8be71e6314cfe09d3167c95f712589e8" + +[[package]] +name = "bitflags" +version = "1.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" + +[[package]] +name = "blake2b_simd" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72936ee4afc7f8f736d1c38383b56480b5497b4617b4a77bdbf1d2ababc76127" +dependencies = [ + "arrayref", + "arrayvec", + "constant_time_eq", +] + +[[package]] +name = "block-buffer" +version = "0.10.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "69cce20737498f97b993470a6e536b8523f0af7892a4f928cceb1ac5e52ebe7e" +dependencies = [ + "generic-array", +] + +[[package]] +name = "brotli" +version = "3.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1a0b1dbcc8ae29329621f8d4f0d835787c1c38bb1401979b49d13b0b305ff68" +dependencies = [ + "alloc-no-stdlib", + "alloc-stdlib", + "brotli-decompressor", +] + +[[package]] +name = "brotli-decompressor" +version = "2.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "59ad2d4653bf5ca36ae797b1f4bb4dbddb60ce49ca4aed8a2ce4829f60425b80" +dependencies = [ + "alloc-no-stdlib", + "alloc-stdlib", +] + +[[package]] +name = "bstr" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba3569f383e8f1598449f1a423e72e99569137b47740b1da11ef19af3d5c3223" +dependencies = [ + "memchr", +] + +[[package]] +name = "bumpalo" +version = "3.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "572f695136211188308f16ad2ca5c851a712c464060ae6974944458eb83880ba" + +[[package]] +name = "byteorder" +version = "1.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "14c189c53d098945499cdfa7ecc63567cf3886b3332b312a5b4585d8d3a6a610" + +[[package]] +name = "bytes" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dfb24e866b15a1af2a1b663f10c6b6b8f397a84aadb828f12e5b289ec23a3a3c" + +[[package]] +name = "bytestring" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f7f83e57d9154148e355404702e2694463241880b939570d7c97c014da7a69a1" +dependencies = [ + "bytes", +] + +[[package]] +name = "cache-buster" +version = "0.2.0" +source = "git+https://github.com/realaravinth/cache-buster#7ca4545722fb99be30698a5e72c7d982a70fa11f" +dependencies = [ + "data-encoding", + "derive_builder", + "mime", + "mime_guess", + "serde", + "serde_json", + "sha2", + "walkdir", +] + +[[package]] +name = "cc" +version = "1.0.77" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e9f73505338f7d905b19d18738976aae232eb46b8efc15554ffc56deb5d9ebe4" +dependencies = [ + "jobserver", +] + +[[package]] +name = "cfg-if" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" + +[[package]] +name = "cipher" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d1873270f8f7942c191139cb8a40fd228da6c3fd2fc376d7e92d47aa14aeb59e" +dependencies = [ + "crypto-common", + "inout", +] + +[[package]] +name = "clap" +version = "3.2.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "71655c45cb9845d3270c9d6df84ebe72b4dad3c2ba3f7023ad47c144e4e473a5" +dependencies = [ + "atty", + "bitflags", + "clap_derive", + "clap_lex", + "indexmap", + "once_cell", + "strsim", + "termcolor", + "textwrap", +] + +[[package]] +name = "clap_derive" +version = "3.2.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ea0c8bce528c4be4da13ea6fead8965e95b6073585a2f05204bd8f4119f82a65" +dependencies = [ + "heck", + "proc-macro-error", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "clap_lex" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2850f2f5a82cbf437dd5af4d49848fbdfc27c157c3d010345776f952765261c5" +dependencies = [ + "os_str_bytes", +] + +[[package]] +name = "config" +version = "0.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "11f1667b8320afa80d69d8bbe40830df2c8a06003d86f73d8e003b2c48df416d" +dependencies = [ + "async-trait", + "json5", + "lazy_static", + "nom", + "pathdiff", + "ron", + "rust-ini", + "serde", + "serde_json", + "toml", + "yaml-rust", +] + +[[package]] +name = "constant_time_eq" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "245097e9a4535ee1e3e3931fcfcd55a796a44c643e8596ff6566d68f09b87bbc" + +[[package]] +name = "convert_case" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6245d59a3e82a7fc217c5828a6692dbc6dfb63a0c8c90495621f7b9d79704a0e" + +[[package]] +name = "cookie" +version = "0.16.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "344adc371239ef32293cb1c4fe519592fcf21206c79c02854320afcdf3ab4917" +dependencies = [ + "aes-gcm", + "base64", + "hkdf", + "hmac", + "percent-encoding", + "rand 0.8.5", + "sha2", + "subtle", + "time", + "version_check", +] + +[[package]] +name = "core-foundation" +version = "0.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "194a7a9e6de53fa55116934067c844d9d749312f75c6f6d0980e8c252f8c2146" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "core-foundation-sys" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5827cebf4670468b8772dd191856768aedcb1b0278a04f989f7766351917b9dc" + +[[package]] +name = "cpufeatures" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "28d997bd5e24a5928dd43e46dc529867e207907fe0b239c3477d924f7f2ca320" +dependencies = [ + "libc", +] + +[[package]] +name = "crc" +version = "3.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53757d12b596c16c78b83458d732a5d1a17ab3f53f2f7412f6fb57cc8a140ab3" +dependencies = [ + "crc-catalog", +] + +[[package]] +name = "crc-catalog" +version = "2.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2d0165d2900ae6778e36e80bbc4da3b5eefccee9ba939761f9c2882a5d9af3ff" + +[[package]] +name = "crc32fast" +version = "1.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b540bd8bc810d3885c6ea91e2018302f68baba2129ab3e88f32389ee9370880d" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "crossbeam-queue" +version = "0.3.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d1cfb3ea8a53f37c40dea2c7bedcbd88bdfae54f5e2175d6ecaff1c988353add" +dependencies = [ + "cfg-if", + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-utils" +version = "0.8.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4fb766fa798726286dbbb842f174001dab8abc7b627a1dd86e0b7222a95d929f" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "crypto-common" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3" +dependencies = [ + "generic-array", + "rand_core 0.6.4", + "typenum", +] + +[[package]] +name = "cssparser" +version = "0.27.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "754b69d351cdc2d8ee09ae203db831e005560fc6030da058f86ad60c92a9cb0a" +dependencies = [ + "cssparser-macros", + "dtoa-short", + "itoa 0.4.8", + "matches", + "phf 0.8.0", + "proc-macro2", + "quote", + "smallvec", + "syn", +] + +[[package]] +name = "cssparser-macros" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dfae75de57f2b2e85e8768c3ea840fd159c8f33e2b6522c7835b7abac81be16e" +dependencies = [ + "quote", + "syn", +] + +[[package]] +name = "ctr" +version = "0.9.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0369ee1ad671834580515889b80f2ea915f23b8be8d0daa4bbaf2ac5c7590835" +dependencies = [ + "cipher", +] + +[[package]] +name = "darling" +version = "0.14.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b0dd3cd20dc6b5a876612a6e5accfe7f3dd883db6d07acfbf14c128f61550dfa" +dependencies = [ + "darling_core", + "darling_macro", +] + +[[package]] +name = "darling_core" +version = "0.14.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a784d2ccaf7c98501746bf0be29b2022ba41fd62a2e622af997a03e9f972859f" +dependencies = [ + "fnv", + "ident_case", + "proc-macro2", + "quote", + "strsim", + "syn", +] + +[[package]] +name = "darling_macro" +version = "0.14.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7618812407e9402654622dd402b0a89dff9ba93badd6540781526117b92aab7e" +dependencies = [ + "darling_core", + "quote", + "syn", +] + +[[package]] +name = "data-encoding" +version = "2.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3ee2393c4a91429dffb4bedf19f4d6abf27d8a732c8ce4980305d782e5426d57" + +[[package]] +name = "derive_builder" +version = "0.11.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d07adf7be193b71cc36b193d0f5fe60b918a3a9db4dad0449f57bcfd519704a3" +dependencies = [ + "derive_builder_macro", +] + +[[package]] +name = "derive_builder_core" +version = "0.11.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1f91d4cfa921f1c05904dc3c57b4a32c38aed3340cce209f3a6fd1478babafc4" +dependencies = [ + "darling", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "derive_builder_macro" +version = "0.11.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f0314b72bed045f3a68671b3c86328386762c93f82d98c65c3cb5e5f573dd68" +dependencies = [ + "derive_builder_core", + "syn", +] + +[[package]] +name = "derive_more" +version = "0.99.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4fb810d30a7c1953f91334de7244731fc3f3c10d7fe163338a35b9f640960321" +dependencies = [ + "convert_case", + "proc-macro2", + "quote", + "rustc_version", + "syn", +] + +[[package]] +name = "digest" +version = "0.10.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8168378f4e5023e7218c89c891c0fd8ecdb5e5e4f18cb78f38cf245dd021e76f" +dependencies = [ + "block-buffer", + "crypto-common", + "subtle", +] + +[[package]] +name = "dlv-list" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0688c2a7f92e427f44895cd63841bff7b29f8d7a1648b9e7e07a4a365b2e1257" + +[[package]] +name = "dotenvy" +version = "0.15.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "03d8c417d7a8cb362e0c37e5d815f5eb7c37f79ff93707329d5a194e42e54ca0" + +[[package]] +name = "dtoa" +version = "0.4.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56899898ce76aaf4a0f24d914c97ea6ed976d42fec6ad33fcbb0a1103e07b2b0" + +[[package]] +name = "dtoa-short" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bde03329ae10e79ede66c9ce4dc930aa8599043b0743008548680f25b91502d6" +dependencies = [ + "dtoa", +] + +[[package]] +name = "ego-tree" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3a68a4904193147e0a8dec3314640e6db742afd5f6e634f428a6af230d9b3591" + +[[package]] +name = "either" +version = "1.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "90e5c1c8368803113bf0c9584fc495a58b86dc8a29edbf8fe877d21d9507e797" +dependencies = [ + "serde", +] + +[[package]] +name = "encoding_rs" +version = "0.8.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9852635589dc9f9ea1b6fe9f05b50ef208c85c834a562f0c6abb1c475736ec2b" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "env_logger" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "44533bbbb3bb3c1fa17d9f2e4e38bbbaf8396ba82193c4cb1b6445d711445d36" +dependencies = [ + "atty", + "humantime", + "log", + "regex", + "termcolor", +] + +[[package]] +name = "event-listener" +version = "2.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0206175f82b8d6bf6652ff7d71a1e27fd2e4efde587fd368662814d6ec1d9ce0" + +[[package]] +name = "fastrand" +version = "1.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a7a407cfaa3385c4ae6b23e84623d48c2798d06e3e6a1878f7f59f17b3f86499" +dependencies = [ + "instant", +] + +[[package]] +name = "flate2" +version = "1.0.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a8a2db397cb1c8772f31494cb8917e48cd1e64f0fa7efac59fbd741a0a8ce841" +dependencies = [ + "crc32fast", + "miniz_oxide", +] + +[[package]] +name = "flume" +version = "0.10.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1657b4441c3403d9f7b3409e47575237dac27b1b5726df654a6ecbf92f0f7577" +dependencies = [ + "futures-core", + "futures-sink", + "pin-project", + "spin 0.9.4", +] + +[[package]] +name = "fnv" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" + +[[package]] +name = "foreign-types" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f6f339eb8adc052cd2ca78910fda869aefa38d22d5cb648e6485e4d3fc06f3b1" +dependencies = [ + "foreign-types-shared", +] + +[[package]] +name = "foreign-types-shared" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "00b0228411908ca8685dba7fc2cdd70ec9990a6e753e89b6ac91a84c40fbaf4b" + +[[package]] +name = "form_urlencoded" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a9c384f161156f5260c24a097c56119f9be8c798586aecc13afbcbe7b7e26bf8" +dependencies = [ + "percent-encoding", +] + +[[package]] +name = "futf" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df420e2e84819663797d1ec6544b13c5be84629e7bb00dc960d6917db2987843" +dependencies = [ + "mac", + "new_debug_unreachable", +] + +[[package]] +name = "futures" +version = "0.3.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38390104763dc37a5145a53c29c63c1290b5d316d6086ec32c293f6736051bb0" +dependencies = [ + "futures-channel", + "futures-core", + "futures-executor", + "futures-io", + "futures-sink", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-channel" +version = "0.3.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52ba265a92256105f45b719605a571ffe2d1f0fea3807304b522c1d778f79eed" +dependencies = [ + "futures-core", + "futures-sink", +] + +[[package]] +name = "futures-core" +version = "0.3.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "04909a7a7e4633ae6c4a9ab280aeb86da1236243a77b694a49eacd659a4bd3ac" + +[[package]] +name = "futures-executor" +version = "0.3.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7acc85df6714c176ab5edf386123fafe217be88c0840ec11f199441134a074e2" +dependencies = [ + "futures-core", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-intrusive" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a604f7a68fbf8103337523b1fadc8ade7361ee3f112f7c680ad179651616aed5" +dependencies = [ + "futures-core", + "lock_api", + "parking_lot 0.11.2", +] + +[[package]] +name = "futures-io" +version = "0.3.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "00f5fb52a06bdcadeb54e8d3671f8888a39697dcb0b81b23b55174030427f4eb" + +[[package]] +name = "futures-macro" +version = "0.3.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bdfb8ce053d86b91919aad980c220b1fb8401a9394410e1c289ed7e66b61835d" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "futures-sink" +version = "0.3.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "39c15cf1a4aa79df40f1bb462fb39676d0ad9e366c2a33b590d7c66f4f81fcf9" + +[[package]] +name = "futures-task" +version = "0.3.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2ffb393ac5d9a6eaa9d3fdf37ae2776656b706e200c8e16b1bdb227f5198e6ea" + +[[package]] +name = "futures-util" +version = "0.3.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "197676987abd2f9cadff84926f410af1c183608d36641465df73ae8211dc65d6" +dependencies = [ + "futures-channel", + "futures-core", + "futures-io", + "futures-macro", + "futures-sink", + "futures-task", + "memchr", + "pin-project-lite", + "pin-utils", + "slab", +] + +[[package]] +name = "fxhash" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c31b6d751ae2c7f11320402d34e41349dd1016f8d5d45e48c4312bc8625af50c" +dependencies = [ + "byteorder", +] + +[[package]] +name = "generic-array" +version = "0.14.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bff49e947297f3312447abdca79f45f4738097cc82b06e72054d2223f601f1b9" +dependencies = [ + "typenum", + "version_check", +] + +[[package]] +name = "getopts" +version = "0.2.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "14dbbfd5c71d70241ecf9e6f13737f7b5ce823821063188d7e46c41d371eebd5" +dependencies = [ + "unicode-width", +] + +[[package]] +name = "getrandom" +version = "0.1.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8fc3cb4d91f53b50155bdcfd23f6a4c39ae1969c2ae85982b135750cccaf5fce" +dependencies = [ + "cfg-if", + "libc", + "wasi 0.9.0+wasi-snapshot-preview1", +] + +[[package]] +name = "getrandom" +version = "0.2.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c05aeb6a22b8f62540c194aac980f2115af067bfe15a0734d7277a768d396b31" +dependencies = [ + "cfg-if", + "libc", + "wasi 0.11.0+wasi-snapshot-preview1", +] + +[[package]] +name = "ghash" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d930750de5717d2dd0b8c0d42c076c0e884c81a73e6cab859bbd2339c71e3e40" +dependencies = [ + "opaque-debug", + "polyval", +] + +[[package]] +name = "globset" +version = "0.4.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0a1e17342619edbc21a964c2afbeb6c820c6a2560032872f397bb97ea127bd0a" +dependencies = [ + "aho-corasick", + "bstr", + "fnv", + "log", + "regex", +] + +[[package]] +name = "globwalk" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93e3af942408868f6934a7b85134a3230832b9977cf66125df2f9edcfce4ddcc" +dependencies = [ + "bitflags", + "ignore", + "walkdir", +] + +[[package]] +name = "h2" +version = "0.3.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5f9f29bc9dda355256b2916cf526ab02ce0aeaaaf2bad60d65ef3f12f11dd0f4" +dependencies = [ + "bytes", + "fnv", + "futures-core", + "futures-sink", + "futures-util", + "http", + "indexmap", + "slab", + "tokio", + "tokio-util", + "tracing", +] + +[[package]] +name = "hashbrown" +version = "0.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" +dependencies = [ + "ahash", +] + +[[package]] +name = "hashlink" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "69fe1fcf8b4278d860ad0548329f892a3631fb63f82574df68275f34cdbe0ffa" +dependencies = [ + "hashbrown", +] + +[[package]] +name = "heck" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2540771e65fc8cb83cd6e8a237f70c319bd5c29f78ed1084ba5d50eeac86f7f9" +dependencies = [ + "unicode-segmentation", +] + +[[package]] +name = "hermit-abi" +version = "0.1.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "62b467343b94ba476dcb2500d242dadbb39557df889310ac77c5d99100aaac33" +dependencies = [ + "libc", +] + +[[package]] +name = "hex" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" + +[[package]] +name = "hkdf" +version = "0.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "791a029f6b9fc27657f6f188ec6e5e43f6911f6f878e0dc5501396e09809d437" +dependencies = [ + "hmac", +] + +[[package]] +name = "hmac" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c49c37c09c17a53d937dfbb742eb3a961d65a994e6bcdcf37e7399d0cc8ab5e" +dependencies = [ + "digest", +] + +[[package]] +name = "html5ever" +version = "0.26.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bea68cab48b8459f17cf1c944c67ddc572d272d9f2b274140f223ecb1da4a3b7" +dependencies = [ + "log", + "mac", + "markup5ever", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "http" +version = "0.2.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75f43d41e26995c17e71ee126451dd3941010b0514a81a9d11f3b341debc2399" +dependencies = [ + "bytes", + "fnv", + "itoa 1.0.4", +] + +[[package]] +name = "http-body" +version = "0.4.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d5f38f16d184e36f2408a55281cd658ecbd3ca05cce6d6510a176eca393e26d1" +dependencies = [ + "bytes", + "http", + "pin-project-lite", +] + +[[package]] +name = "httparse" +version = "1.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d897f394bad6a705d5f4104762e116a75639e470d80901eed05a860a95cb1904" + +[[package]] +name = "httpdate" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c4a1e36c821dbe04574f602848a19f742f4fb3c98d40449f11bcad18d6b17421" + +[[package]] +name = "humantime" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df004cfca50ef23c36850aaaa59ad52cc70d0e90243c3c7737a4dd32dc7a3c4f" +dependencies = [ + "quick-error", +] + +[[package]] +name = "hyper" +version = "0.14.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "034711faac9d2166cb1baf1a2fb0b60b1f277f8492fd72176c17f3515e1abd3c" +dependencies = [ + "bytes", + "futures-channel", + "futures-core", + "futures-util", + "h2", + "http", + "http-body", + "httparse", + "httpdate", + "itoa 1.0.4", + "pin-project-lite", + "socket2", + "tokio", + "tower-service", + "tracing", + "want", +] + +[[package]] +name = "hyper-rustls" +version = "0.23.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "59df7c4e19c950e6e0e868dcc0a300b09a9b88e9ec55bd879ca819087a77355d" +dependencies = [ + "http", + "hyper", + "rustls", + "tokio", + "tokio-rustls", +] + +[[package]] +name = "hyper-tls" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d6183ddfa99b85da61a140bea0efc93fdf56ceaa041b37d553518030827f9905" +dependencies = [ + "bytes", + "hyper", + "native-tls", + "tokio", + "tokio-native-tls", +] + +[[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" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "418a0a6fab821475f634efe3ccc45c013f742efe03d853e8d3355d5cb850ecf8" +dependencies = [ + "matches", + "unicode-bidi", + "unicode-normalization", +] + +[[package]] +name = "idna" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e14ddfc70884202db2244c223200c204c2bda1bc6e0998d11b5e024d657209e6" +dependencies = [ + "unicode-bidi", + "unicode-normalization", +] + +[[package]] +name = "if_chain" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb56e1aa765b4b4f3aadfab769793b7087bb03a4ea4920644a6d238e2df5b9ed" + +[[package]] +name = "ignore" +version = "0.4.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "713f1b139373f96a2e0ce3ac931cd01ee973c3c5dd7c40c0c2efe96ad2b6751d" +dependencies = [ + "crossbeam-utils", + "globset", + "lazy_static", + "log", + "memchr", + "regex", + "same-file", + "thread_local", + "walkdir", + "winapi-util", +] + +[[package]] +name = "indexmap" +version = "1.9.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1885e79c1fc4b10f0e172c475f458b7f7b93061064d98c3293e98c5ba0c8b399" +dependencies = [ + "autocfg", + "hashbrown", +] + +[[package]] +name = "inout" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a0c10553d664a4d0bcff9f4215d0aac67a639cc68ef660840afe309b807bc9f5" +dependencies = [ + "generic-array", +] + +[[package]] +name = "instant" +version = "0.1.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a5bbe824c507c5da5956355e86a746d82e0e1464f65d862cc5e71da70e94b2c" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "ipnet" +version = "2.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f88c5561171189e69df9d98bcf18fd5f9558300f7ea7b801eb8a0fd748bd8745" + +[[package]] +name = "itertools" +version = "0.10.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b0fd2260e829bddf4cb6ea802289de2f86d6a7a690192fbe91b3f46e0f2c8473" +dependencies = [ + "either", +] + +[[package]] +name = "itoa" +version = "0.4.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b71991ff56294aa922b450139ee08b3bfc70982c6b2c7562771375cf73542dd4" + +[[package]] +name = "itoa" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4217ad341ebadf8d8e724e264f13e593e0648f5b3e94b3896a5df283be015ecc" + +[[package]] +name = "jobserver" +version = "0.1.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "068b1ee6743e4d11fb9c6a1e6064b3693a1b600e7f5f5988047d98b3dc9fb90b" +dependencies = [ + "libc", +] + +[[package]] +name = "js-sys" +version = "0.3.60" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "49409df3e3bf0856b916e2ceaca09ee28e6871cf7d9ce97a692cacfdb2a25a47" +dependencies = [ + "wasm-bindgen", +] + +[[package]] +name = "json5" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96b0db21af676c1ce64250b5f40f3ce2cf27e4e47cb91ed91eb6fe9350b430c1" +dependencies = [ + "pest", + "pest_derive", + "serde", +] + +[[package]] +name = "language-tags" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d4345964bb142484797b161f473a503a434de77149dd8c7427788c6e13379388" + +[[package]] +name = "lazy_static" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" + +[[package]] +name = "lazycell" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "830d08ce1d1d941e6b30645f1a0eb5643013d835ce3779a5fc208261dbe10f55" + +[[package]] +name = "libc" +version = "0.2.137" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc7fcc620a3bff7cdd7a365be3376c97191aeaccc2a603e600951e452615bf89" + +[[package]] +name = "libsqlite3-sys" +version = "0.24.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "898745e570c7d0453cc1fbc4a701eb6c662ed54e8fec8b7d14be137ebeeb9d14" +dependencies = [ + "cc", + "pkg-config", + "vcpkg", +] + +[[package]] +name = "linked-hash-map" +version = "0.5.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0717cef1bc8b636c6e1c1bbdefc09e6322da8a9321966e8928ef80d20f7f770f" + +[[package]] +name = "local-channel" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f303ec0e94c6c54447f84f3b0ef7af769858a9c4ef56ef2a986d3dcd4c3fc9c" +dependencies = [ + "futures-core", + "futures-sink", + "futures-util", + "local-waker", +] + +[[package]] +name = "local-waker" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e34f76eb3611940e0e7d53a9aaa4e6a3151f69541a282fd0dad5571420c53ff1" + +[[package]] +name = "lock_api" +version = "0.4.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "435011366fe56583b16cf956f9df0095b405b82d76425bc8981c0e22e60ec4df" +dependencies = [ + "autocfg", + "scopeguard", +] + +[[package]] +name = "log" +version = "0.4.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "abb12e687cfb44aa40f41fc3978ef76448f9b6038cad6aef4259d3c095a2382e" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "lol_html" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09ff2adf9c54f4de7d66a9177ea7d27d5b8108503bb03d5b719869b8f4bc2ab2" +dependencies = [ + "bitflags", + "cfg-if", + "cssparser", + "encoding_rs", + "hashbrown", + "lazy_static", + "lazycell", + "memchr", + "safemem", + "selectors", + "thiserror", +] + +[[package]] +name = "mac" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c41e0c4fef86961ac6d6f8a82609f55f31b05e4fce149ac5710e439df7619ba4" + +[[package]] +name = "maplit" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3e2e65a1a2e43cfcb47a895c4c8b10d1f4a61097f9f254f183aee60cad9c651d" + +[[package]] +name = "markup5ever" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a2629bb1404f3d34c2e921f21fd34ba00b206124c81f65c50b43b6aaefeb016" +dependencies = [ + "log", + "phf 0.10.1", + "phf_codegen 0.10.0", + "string_cache", + "string_cache_codegen", + "tendril", +] + +[[package]] +name = "matches" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a3e378b66a060d48947b590737b30a1be76706c8dd7b8ba0f2fe3989c68a853f" + +[[package]] +name = "memchr" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2dffe52ecf27772e601905b7522cb4ef790d2cc203488bbd0e2fe85fcb74566d" + +[[package]] +name = "mime" +version = "0.3.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2a60c7ce501c71e03a9c9c0d35b861413ae925bd979cc7a4e30d060069aaac8d" + +[[package]] +name = "mime_guess" +version = "2.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4192263c238a5f0d0c6bfd21f336a313a4ce1c450542449ca191bb657b4642ef" +dependencies = [ + "mime", + "unicase", +] + +[[package]] +name = "minimal-lexical" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" + +[[package]] +name = "miniz_oxide" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b275950c28b37e794e8c55d88aeb5e139d0ce23fdbbeda68f8d7174abdf9e8fa" +dependencies = [ + "adler", +] + +[[package]] +name = "mio" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e5d732bc30207a6423068df043e3d02e0735b155ad7ce1a6f76fe2baa5b158de" +dependencies = [ + "libc", + "log", + "wasi 0.11.0+wasi-snapshot-preview1", + "windows-sys 0.42.0", +] + +[[package]] +name = "mktemp" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "975de676448231fcde04b9149d2543077e166b78fc29eae5aa219e7928410da2" +dependencies = [ + "uuid 0.8.2", +] + +[[package]] +name = "native-tls" +version = "0.2.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "07226173c32f2926027b63cce4bcd8076c3552846cbe7925f3aaffeac0a3b92e" +dependencies = [ + "lazy_static", + "libc", + "log", + "openssl", + "openssl-probe", + "openssl-sys", + "schannel", + "security-framework", + "security-framework-sys", + "tempfile", +] + +[[package]] +name = "new_debug_unreachable" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e4a24736216ec316047a1fc4252e27dabb04218aa4a3f37c6e7ddbf1f9782b54" + +[[package]] +name = "nodrop" +version = "0.1.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72ef4a56884ca558e5ddb05a1d1e7e1bfd9a68d9ed024c21704cc98872dae1bb" + +[[package]] +name = "nom" +version = "7.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a8903e5a29a317527874d0402f867152a3d21c908bb0b933e416c65e301d4c36" +dependencies = [ + "memchr", + "minimal-lexical", +] + +[[package]] +name = "num-traits" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "578ede34cf02f8924ab9447f50c28075b4d3e5b269972345e7e0372b38c6cdcd" +dependencies = [ + "autocfg", +] + +[[package]] +name = "num_cpus" +version = "1.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f6058e64324c71e02bc2b150e4f3bc8286db6c83092132ffa3f6b1eab0f9def5" +dependencies = [ + "hermit-abi", + "libc", +] + +[[package]] +name = "num_enum" +version = "0.5.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf5395665662ef45796a4ff5486c5d41d29e0c09640af4c5f17fd94ee2c119c9" +dependencies = [ + "num_enum_derive", +] + +[[package]] +name = "num_enum_derive" +version = "0.5.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b0498641e53dd6ac1a4f22547548caa6864cc4933784319cd1775271c5a46ce" +dependencies = [ + "proc-macro-crate", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "once_cell" +version = "1.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "86f0b0d4bf799edbc74508c1e8bf170ff5f41238e5f8225603ca7caaae2b7860" + +[[package]] +name = "opaque-debug" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "624a8340c38c1b80fd549087862da4ba43e08858af025b236e509b6649fc13d5" + +[[package]] +name = "openssl" +version = "0.10.43" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "020433887e44c27ff16365eaa2d380547a94544ad509aff6eb5b6e3e0b27b376" +dependencies = [ + "bitflags", + "cfg-if", + "foreign-types", + "libc", + "once_cell", + "openssl-macros", + "openssl-sys", +] + +[[package]] +name = "openssl-macros" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b501e44f11665960c7e7fcf062c7d96a14ade4aa98116c004b2e37b5be7d736c" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "openssl-probe" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff011a302c396a5197692431fc1948019154afc178baf7d8e37367442a4601cf" + +[[package]] +name = "openssl-sys" +version = "0.9.78" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "07d5c8cb6e57b3a3612064d7b18b117912b4ce70955c2504d4b741c9e244b132" +dependencies = [ + "autocfg", + "cc", + "libc", + "pkg-config", + "vcpkg", +] + +[[package]] +name = "ordered-multimap" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ccd746e37177e1711c20dd619a1620f34f5c8b569c53590a72dedd5344d8924a" +dependencies = [ + "dlv-list", + "hashbrown", +] + +[[package]] +name = "os_str_bytes" +version = "6.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b7820b9daea5457c9f21c69448905d723fbd21136ccf521748f23fd49e723ee" + +[[package]] +name = "parking_lot" +version = "0.11.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7d17b78036a60663b797adeaee46f5c9dfebb86948d1255007a1d6be0271ff99" +dependencies = [ + "instant", + "lock_api", + "parking_lot_core 0.8.5", +] + +[[package]] +name = "parking_lot" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3742b2c103b9f06bc9fff0a37ff4912935851bee6d36f3c02bcc755bcfec228f" +dependencies = [ + "lock_api", + "parking_lot_core 0.9.5", +] + +[[package]] +name = "parking_lot_core" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d76e8e1493bcac0d2766c42737f34458f1c8c50c0d23bcb24ea953affb273216" +dependencies = [ + "cfg-if", + "instant", + "libc", + "redox_syscall", + "smallvec", + "winapi", +] + +[[package]] +name = "parking_lot_core" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7ff9f3fef3968a3ec5945535ed654cb38ff72d7495a25619e2247fb15a2ed9ba" +dependencies = [ + "cfg-if", + "libc", + "redox_syscall", + "smallvec", + "windows-sys 0.42.0", +] + +[[package]] +name = "paste" +version = "1.0.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b1de2e551fb905ac83f73f7aedf2f0cb4a0da7e35efa24a202a936269f1f18e1" + +[[package]] +name = "pathdiff" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8835116a5c179084a830efb3adc117ab007512b535bc1a21c991d3b32a6b44dd" + +[[package]] +name = "pativu" +version = "0.1.0" +dependencies = [ + "actix-auth-middleware", + "actix-http", + "actix-identity", + "actix-rt", + "actix-web", + "actix-web-codegen-const-routes", + "argon2-creds", + "cache-buster", + "clap", + "config", + "derive_more", + "futures", + "futures-util", + "lazy_static", + "lol_html", + "mime", + "mime_guess", + "mktemp", + "num_cpus", + "num_enum", + "pretty_env_logger", + "rand 0.8.5", + "reqwest", + "rust-embed", + "scraper", + "serde", + "serde_json", + "serde_yaml", + "sqlx", + "tera", + "tokio", + "toml", + "tracing", + "tracing-actix-web", + "url", + "urlencoding", + "uuid 1.2.2", +] + +[[package]] +name = "percent-encoding" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "478c572c3d73181ff3c2539045f6eb99e5491218eae919370993b890cdbdd98e" + +[[package]] +name = "pest" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5f400b0f7905bf702f9f3dc3df5a121b16c54e9e8012c082905fdf09a931861a" +dependencies = [ + "thiserror", + "ucd-trie", +] + +[[package]] +name = "pest_derive" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "423c2ba011d6e27b02b482a3707c773d19aec65cc024637aec44e19652e66f63" +dependencies = [ + "pest", + "pest_generator", +] + +[[package]] +name = "pest_generator" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3e64e6c2c85031c02fdbd9e5c72845445ca0a724d419aa0bc068ac620c9935c1" +dependencies = [ + "pest", + "pest_meta", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "pest_meta" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "57959b91f0a133f89a68be874a5c88ed689c19cd729ecdb5d762ebf16c64d662" +dependencies = [ + "once_cell", + "pest", + "sha1", +] + +[[package]] +name = "phf" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3dfb61232e34fcb633f43d12c58f83c1df82962dcdfa565a4e866ffc17dafe12" +dependencies = [ + "phf_macros", + "phf_shared 0.8.0", + "proc-macro-hack", +] + +[[package]] +name = "phf" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fabbf1ead8a5bcbc20f5f8b939ee3f5b0f6f281b6ad3468b84656b658b455259" +dependencies = [ + "phf_shared 0.10.0", +] + +[[package]] +name = "phf_codegen" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cbffee61585b0411840d3ece935cce9cb6321f01c45477d30066498cd5e1a815" +dependencies = [ + "phf_generator 0.8.0", + "phf_shared 0.8.0", +] + +[[package]] +name = "phf_codegen" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4fb1c3a8bc4dd4e5cfce29b44ffc14bedd2ee294559a294e2a4d4c9e9a6a13cd" +dependencies = [ + "phf_generator 0.10.0", + "phf_shared 0.10.0", +] + +[[package]] +name = "phf_generator" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "17367f0cc86f2d25802b2c26ee58a7b23faeccf78a396094c13dced0d0182526" +dependencies = [ + "phf_shared 0.8.0", + "rand 0.7.3", +] + +[[package]] +name = "phf_generator" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d5285893bb5eb82e6aaf5d59ee909a06a16737a8970984dd7746ba9283498d6" +dependencies = [ + "phf_shared 0.10.0", + "rand 0.8.5", +] + +[[package]] +name = "phf_macros" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f6fde18ff429ffc8fe78e2bf7f8b7a5a5a6e2a8b58bc5a9ac69198bbda9189c" +dependencies = [ + "phf_generator 0.8.0", + "phf_shared 0.8.0", + "proc-macro-hack", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "phf_shared" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c00cf8b9eafe68dde5e9eaa2cef8ee84a9336a47d566ec55ca16589633b65af7" +dependencies = [ + "siphasher", +] + +[[package]] +name = "phf_shared" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6796ad771acdc0123d2a88dc428b5e38ef24456743ddb1744ed628f9815c096" +dependencies = [ + "siphasher", +] + +[[package]] +name = "pin-project" +version = "1.0.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ad29a609b6bcd67fee905812e544992d216af9d755757c05ed2d0e15a74c6ecc" +dependencies = [ + "pin-project-internal", +] + +[[package]] +name = "pin-project-internal" +version = "1.0.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "069bdb1e05adc7a8990dce9cc75370895fbe4e3d58b9b73bf1aee56359344a55" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "pin-project-lite" +version = "0.2.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e0a7ae3ac2f1173085d398531c705756c94a4c56843785df85a60c1a0afac116" + +[[package]] +name = "pin-utils" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" + +[[package]] +name = "pkg-config" +version = "0.3.26" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ac9a59f73473f1b8d852421e59e64809f025994837ef743615c6d0c5b305160" + +[[package]] +name = "polyval" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7ef234e08c11dfcb2e56f79fd70f6f2eb7f025c0ce2333e82f4f0518ecad30c6" +dependencies = [ + "cfg-if", + "cpufeatures", + "opaque-debug", + "universal-hash", +] + +[[package]] +name = "ppv-lite86" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b40af805b3121feab8a3c29f04d8ad262fa8e0561883e7653e024ae4479e6de" + +[[package]] +name = "precomputed-hash" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "925383efa346730478fb4838dbe9137d2a47675ad789c546d150a6e1dd4ab31c" + +[[package]] +name = "pretty_env_logger" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "926d36b9553851b8b0005f1275891b392ee4d2d833852c417ed025477350fb9d" +dependencies = [ + "env_logger", + "log", +] + +[[package]] +name = "proc-macro-crate" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eda0fc3b0fb7c975631757e14d9049da17374063edb6ebbcbc54d880d4fe94e9" +dependencies = [ + "once_cell", + "thiserror", + "toml", +] + +[[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", +] + +[[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", +] + +[[package]] +name = "proc-macro-hack" +version = "0.5.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dbf0c48bc1d91375ae5c3cd81e3722dff1abcf81a30960240640d223f59fe0e5" + +[[package]] +name = "proc-macro2" +version = "1.0.47" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5ea3d908b0e36316caf9e9e2c4625cdde190a7e6f440d794667ed17a1855e725" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quick-error" +version = "1.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1d01941d82fa2ab50be1e79e6714289dd7cde78eba4c074bc5a4374f650dfe0" + +[[package]] +name = "quote" +version = "1.0.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbe448f377a7d6961e30f5955f9b8d106c3f5e449d493ee1b125c1d43c2b5179" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "rand" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6a6b1679d49b24bbfe0c803429aa1874472f50d9b363131f0e89fc356b544d03" +dependencies = [ + "getrandom 0.1.16", + "libc", + "rand_chacha 0.2.2", + "rand_core 0.5.1", + "rand_hc", + "rand_pcg", +] + +[[package]] +name = "rand" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" +dependencies = [ + "libc", + "rand_chacha 0.3.1", + "rand_core 0.6.4", +] + +[[package]] +name = "rand_chacha" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f4c8ed856279c9737206bf725bf36935d8666ead7aa69b52be55af369d193402" +dependencies = [ + "ppv-lite86", + "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.4", +] + +[[package]] +name = "rand_core" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "90bde5296fc891b0cef12a6d03ddccc162ce7b2aff54160af9338f8d40df6d19" +dependencies = [ + "getrandom 0.1.16", +] + +[[package]] +name = "rand_core" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" +dependencies = [ + "getrandom 0.2.8", +] + +[[package]] +name = "rand_hc" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ca3129af7b92a17112d59ad498c6f81eaf463253766b90396d39ea7a39d6613c" +dependencies = [ + "rand_core 0.5.1", +] + +[[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]] +name = "redox_syscall" +version = "0.2.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fb5a58c1855b4b6819d59012155603f0b22ad30cad752600aadfcb695265519a" +dependencies = [ + "bitflags", +] + +[[package]] +name = "regex" +version = "1.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e076559ef8e241f2ae3479e36f97bd5741c0330689e217ad51ce2c76808b868a" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax", +] + +[[package]] +name = "regex-syntax" +version = "0.6.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "456c603be3e8d448b072f410900c09faf164fbce2d480456f50eea6e25f9c848" + +[[package]] +name = "remove_dir_all" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3acd125665422973a33ac9d3dd2df85edad0f4ae9b00dafb1a05e43a9f5ef8e7" +dependencies = [ + "winapi", +] + +[[package]] +name = "reqwest" +version = "0.11.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68cc60575865c7831548863cc02356512e3f1dc2f3f82cb837d7fc4cc8f3c97c" +dependencies = [ + "async-compression", + "base64", + "bytes", + "encoding_rs", + "futures-core", + "futures-util", + "h2", + "http", + "http-body", + "hyper", + "hyper-rustls", + "hyper-tls", + "ipnet", + "js-sys", + "log", + "mime", + "native-tls", + "once_cell", + "percent-encoding", + "pin-project-lite", + "rustls", + "rustls-native-certs", + "rustls-pemfile", + "serde", + "serde_json", + "serde_urlencoded", + "tokio", + "tokio-native-tls", + "tokio-rustls", + "tokio-util", + "tower-service", + "url", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", + "winreg", +] + +[[package]] +name = "ring" +version = "0.16.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3053cf52e236a3ed746dfc745aa9cacf1b791d846bdaf412f60a8d7d6e17c8fc" +dependencies = [ + "cc", + "libc", + "once_cell", + "spin 0.5.2", + "untrusted", + "web-sys", + "winapi", +] + +[[package]] +name = "ron" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "88073939a61e5b7680558e6be56b419e208420c2adb92be54921fa6b72283f1a" +dependencies = [ + "base64", + "bitflags", + "serde", +] + +[[package]] +name = "rust-argon2" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b50162d19404029c1ceca6f6980fe40d45c8b369f6f44446fa14bb39573b5bb9" +dependencies = [ + "base64", + "blake2b_simd", + "constant_time_eq", + "crossbeam-utils", +] + +[[package]] +name = "rust-embed" +version = "6.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "283ffe2f866869428c92e0d61c2f35dfb4355293cdfdc48f49e895c15f1333d1" +dependencies = [ + "rust-embed-impl", + "rust-embed-utils", + "walkdir", +] + +[[package]] +name = "rust-embed-impl" +version = "6.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "31ab23d42d71fb9be1b643fe6765d292c5e14d46912d13f3ae2815ca048ea04d" +dependencies = [ + "proc-macro2", + "quote", + "rust-embed-utils", + "syn", + "walkdir", +] + +[[package]] +name = "rust-embed-utils" +version = "7.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c1669d81dfabd1b5f8e2856b8bbe146c6192b0ba22162edc738ac0a5de18f054" +dependencies = [ + "sha2", + "walkdir", +] + +[[package]] +name = "rust-ini" +version = "0.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f6d5f2436026b4f6e79dc829837d467cc7e9a55ee40e750d716713540715a2df" +dependencies = [ + "cfg-if", + "ordered-multimap", +] + +[[package]] +name = "rustc_version" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bfa0f585226d2e68097d4f95d113b15b83a82e819ab25717ec0590d9584ef366" +dependencies = [ + "semver", +] + +[[package]] +name = "rustls" +version = "0.20.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "539a2bfe908f471bfa933876bd1eb6a19cf2176d375f82ef7f99530a40e48c2c" +dependencies = [ + "log", + "ring", + "sct", + "webpki", +] + +[[package]] +name = "rustls-native-certs" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0167bac7a9f490495f3c33013e7722b53cb087ecbe082fb0c6387c96f634ea50" +dependencies = [ + "openssl-probe", + "rustls-pemfile", + "schannel", + "security-framework", +] + +[[package]] +name = "rustls-pemfile" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0864aeff53f8c05aa08d86e5ef839d3dfcf07aeba2db32f12db0ef716e87bd55" +dependencies = [ + "base64", +] + +[[package]] +name = "ryu" +version = "1.0.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4501abdff3ae82a1c1b477a17252eb69cee9e66eb915c1abaa4f44d873df9f09" + +[[package]] +name = "safemem" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef703b7cb59335eae2eb93ceb664c0eb7ea6bf567079d843e09420219668e072" + +[[package]] +name = "same-file" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502" +dependencies = [ + "winapi-util", +] + +[[package]] +name = "schannel" +version = "0.1.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "88d6731146462ea25d9244b2ed5fd1d716d25c52e4d54aa4fb0f3c4e9854dbe2" +dependencies = [ + "lazy_static", + "windows-sys 0.36.1", +] + +[[package]] +name = "scopeguard" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d29ab0c6d3fc0ee92fe66e2d99f700eab17a8d57d1c1d3b748380fb20baa78cd" + +[[package]] +name = "scraper" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5684396b456f3eb69ceeb34d1b5cb1a2f6acf7ca4452131efa3ba0ee2c2d0a70" +dependencies = [ + "cssparser", + "ego-tree", + "getopts", + "html5ever", + "matches", + "selectors", + "smallvec", + "tendril", +] + +[[package]] +name = "sct" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d53dcdb7c9f8158937a7981b48accfd39a43af418591a5d008c7b22b5e1b7ca4" +dependencies = [ + "ring", + "untrusted", +] + +[[package]] +name = "security-framework" +version = "2.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2bc1bb97804af6631813c55739f771071e0f2ed33ee20b68c86ec505d906356c" +dependencies = [ + "bitflags", + "core-foundation", + "core-foundation-sys", + "libc", + "security-framework-sys", +] + +[[package]] +name = "security-framework-sys" +version = "2.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0160a13a177a45bfb43ce71c01580998474f556ad854dcbca936dd2841a5c556" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "selectors" +version = "0.22.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df320f1889ac4ba6bc0cdc9c9af7af4bd64bb927bccdf32d81140dc1f9be12fe" +dependencies = [ + "bitflags", + "cssparser", + "derive_more", + "fxhash", + "log", + "matches", + "phf 0.8.0", + "phf_codegen 0.8.0", + "precomputed-hash", + "servo_arc", + "smallvec", + "thin-slice", +] + +[[package]] +name = "semver" +version = "1.0.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e25dfac463d778e353db5be2449d1cce89bd6fd23c9f1ea21310ce6e5a1b29c4" + +[[package]] +name = "serde" +version = "1.0.148" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e53f64bb4ba0191d6d0676e1b141ca55047d83b74f5607e6d8eb88126c52c2dc" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.148" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a55492425aa53521babf6137309e7d34c20bbfbbfcfe2c7f3a047fd1f6b92c0c" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "serde_json" +version = "1.0.89" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "020ff22c755c2ed3f8cf162dbb41a7268d934702f3ed3631656ea597e08fc3db" +dependencies = [ + "itoa 1.0.4", + "ryu", + "serde", +] + +[[package]] +name = "serde_urlencoded" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3491c14715ca2294c4d6a88f15e84739788c1d030eed8c110436aafdaa2f3fd" +dependencies = [ + "form_urlencoded", + "itoa 1.0.4", + "ryu", + "serde", +] + +[[package]] +name = "serde_yaml" +version = "0.9.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6d232d893b10de3eb7258ff01974d6ee20663d8e833263c99409d4b13a0209da" +dependencies = [ + "indexmap", + "itoa 1.0.4", + "ryu", + "serde", + "unsafe-libyaml", +] + +[[package]] +name = "servo_arc" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d98238b800e0d1576d8b6e3de32827c2d74bee68bb97748dcf5071fb53965432" +dependencies = [ + "nodrop", + "stable_deref_trait", +] + +[[package]] +name = "sha1" +version = "0.10.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f04293dc80c3993519f2d7f6f511707ee7094fe0c6d3406feb330cdb3540eba3" +dependencies = [ + "cfg-if", + "cpufeatures", + "digest", +] + +[[package]] +name = "sha2" +version = "0.10.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "82e6b795fe2e3b1e845bafcb27aa35405c4d47cdfc92af5fc8d3002f76cebdc0" +dependencies = [ + "cfg-if", + "cpufeatures", + "digest", +] + +[[package]] +name = "signal-hook-registry" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e51e73328dc4ac0c7ccbda3a494dfa03df1de2f46018127f60c693f2648455b0" +dependencies = [ + "libc", +] + +[[package]] +name = "siphasher" +version = "0.3.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7bd3e3206899af3f8b12af284fafc038cc1dc2b41d1b89dd17297221c5d225de" + +[[package]] +name = "slab" +version = "0.4.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4614a76b2a8be0058caa9dbbaf66d988527d86d003c11a94fbd335d7661edcef" +dependencies = [ + "autocfg", +] + +[[package]] +name = "smallvec" +version = "1.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a507befe795404456341dfab10cef66ead4c041f62b8b11bbb92bffe5d0953e0" + +[[package]] +name = "socket2" +version = "0.4.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "02e2d2db9033d13a1567121ddd7a095ee144db4e1ca1b1bda3419bc0da294ebd" +dependencies = [ + "libc", + "winapi", +] + +[[package]] +name = "spin" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e63cff320ae2c57904679ba7cb63280a3dc4613885beafb148ee7bf9aa9042d" + +[[package]] +name = "spin" +version = "0.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f6002a767bff9e83f8eeecf883ecb8011875a21ae8da43bffb817a57e78cc09" +dependencies = [ + "lock_api", +] + +[[package]] +name = "sqlformat" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f87e292b4291f154971a43c3774364e2cbcaec599d3f5bf6fa9d122885dbc38a" +dependencies = [ + "itertools", + "nom", + "unicode_categories", +] + +[[package]] +name = "sqlx" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9249290c05928352f71c077cc44a464d880c63f26f7534728cca008e135c0428" +dependencies = [ + "sqlx-core", + "sqlx-macros", +] + +[[package]] +name = "sqlx-core" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dcbc16ddba161afc99e14d1713a453747a2b07fc097d2009f4c300ec99286105" +dependencies = [ + "ahash", + "atoi", + "bitflags", + "byteorder", + "bytes", + "crc", + "crossbeam-queue", + "dotenvy", + "either", + "event-listener", + "flume", + "futures-channel", + "futures-core", + "futures-executor", + "futures-intrusive", + "futures-util", + "hashlink", + "hex", + "indexmap", + "itoa 1.0.4", + "libc", + "libsqlite3-sys", + "log", + "memchr", + "once_cell", + "paste", + "percent-encoding", + "rustls", + "rustls-pemfile", + "serde", + "serde_json", + "sha2", + "smallvec", + "sqlformat", + "sqlx-rt", + "stringprep", + "thiserror", + "time", + "tokio-stream", + "url", + "uuid 1.2.2", + "webpki-roots", +] + +[[package]] +name = "sqlx-macros" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b850fa514dc11f2ee85be9d055c512aa866746adfacd1cb42d867d68e6a5b0d9" +dependencies = [ + "dotenvy", + "either", + "heck", + "hex", + "once_cell", + "proc-macro2", + "quote", + "serde", + "serde_json", + "sha2", + "sqlx-core", + "sqlx-rt", + "syn", + "url", +] + +[[package]] +name = "sqlx-rt" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24c5b2d25fa654cc5f841750b8e1cdedbe21189bf9a9382ee90bfa9dd3562396" +dependencies = [ + "once_cell", + "tokio", + "tokio-rustls", +] + +[[package]] +name = "stable_deref_trait" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a8f112729512f8e442d81f95a8a7ddf2b7c6b8a1a6f509a95864142b30cab2d3" + +[[package]] +name = "string_cache" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "213494b7a2b503146286049378ce02b482200519accc31872ee8be91fa820a08" +dependencies = [ + "new_debug_unreachable", + "once_cell", + "parking_lot 0.12.1", + "phf_shared 0.10.0", + "precomputed-hash", + "serde", +] + +[[package]] +name = "string_cache_codegen" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6bb30289b722be4ff74a408c3cc27edeaad656e06cb1fe8fa9231fa59c728988" +dependencies = [ + "phf_generator 0.10.0", + "phf_shared 0.10.0", + "proc-macro2", + "quote", +] + +[[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.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.105" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "60b9b43d45702de4c839cb9b51d9f529c5dd26a4aff255b42b1ebc03e88ee908" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "tempfile" +version = "3.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5cdb1ef4eaeeaddc8fbd371e5017057064af0911902ef36b39801f67cc6d79e4" +dependencies = [ + "cfg-if", + "fastrand", + "libc", + "redox_syscall", + "remove_dir_all", + "winapi", +] + +[[package]] +name = "tendril" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d24a120c5fc464a3458240ee02c299ebcb9d67b5249c8848b09d639dca8d7bb0" +dependencies = [ + "futf", + "mac", + "utf-8", +] + +[[package]] +name = "tera" +version = "1.17.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3df578c295f9ec044ff1c829daf31bb7581d5b3c2a7a3d87419afe1f2531438c" +dependencies = [ + "globwalk", + "lazy_static", + "pest", + "pest_derive", + "regex", + "serde", + "serde_json", + "unic-segment", +] + +[[package]] +name = "termcolor" +version = "1.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bab24d30b911b2376f3a13cc2cd443142f0c81dda04c118693e35b3835757755" +dependencies = [ + "winapi-util", +] + +[[package]] +name = "textwrap" +version = "0.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "222a222a5bfe1bba4a77b45ec488a741b3cb8872e5e499451fd7d0129c9c7c3d" + +[[package]] +name = "thin-slice" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8eaa81235c7058867fa8c0e7314f33dcce9c215f535d1913822a2b3f5e289f3c" + +[[package]] +name = "thiserror" +version = "1.0.37" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "10deb33631e3c9018b9baf9dcbbc4f737320d2b576bac10f6aefa048fa407e3e" +dependencies = [ + "thiserror-impl", +] + +[[package]] +name = "thiserror-impl" +version = "1.0.37" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "982d17546b47146b28f7c22e3d08465f6b8903d0ea13c1660d9d84a6e7adcdbb" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "thread_local" +version = "1.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5516c27b78311c50bf42c071425c560ac799b11c30b31f87e3081965fe5e0180" +dependencies = [ + "once_cell", +] + +[[package]] +name = "time" +version = "0.3.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a561bf4617eebd33bca6434b988f39ed798e527f51a1e797d0ee4f61c0a38376" +dependencies = [ + "itoa 1.0.4", + "serde", + "time-core", + "time-macros", +] + +[[package]] +name = "time-core" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2e153e1f1acaef8acc537e68b44906d2db6436e2b35ac2c6b42640fff91f00fd" + +[[package]] +name = "time-macros" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d967f99f534ca7e495c575c62638eebc2898a8c84c119b89e250477bc4ba16b2" +dependencies = [ + "time-core", +] + +[[package]] +name = "tinyvec" +version = "1.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "87cc5ceb3875bb20c2890005a4e226a4651264a5c75edb2421b52861a0a0cb50" +dependencies = [ + "tinyvec_macros", +] + +[[package]] +name = "tinyvec_macros" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cda74da7e1a664f795bb1f8a87ec406fb89a02522cf6e50620d016add6dbbf5c" + +[[package]] +name = "tokio" +version = "1.22.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d76ce4a75fb488c605c54bf610f221cea8b0dafb53333c1a67e8ee199dcd2ae3" +dependencies = [ + "autocfg", + "bytes", + "libc", + "memchr", + "mio", + "num_cpus", + "parking_lot 0.12.1", + "pin-project-lite", + "signal-hook-registry", + "socket2", + "winapi", +] + +[[package]] +name = "tokio-native-tls" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f7d995660bd2b7f8c1568414c1126076c13fbb725c40112dc0120b78eb9b717b" +dependencies = [ + "native-tls", + "tokio", +] + +[[package]] +name = "tokio-rustls" +version = "0.23.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c43ee83903113e03984cb9e5cebe6c04a5116269e900e3ddba8f068a62adda59" +dependencies = [ + "rustls", + "tokio", + "webpki", +] + +[[package]] +name = "tokio-stream" +version = "0.1.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d660770404473ccd7bc9f8b28494a811bc18542b915c0855c51e8f419d5223ce" +dependencies = [ + "futures-core", + "pin-project-lite", + "tokio", +] + +[[package]] +name = "tokio-util" +version = "0.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0bb2e075f03b3d66d8d8785356224ba688d2906a371015e225beeb65ca92c740" +dependencies = [ + "bytes", + "futures-core", + "futures-sink", + "pin-project-lite", + "tokio", + "tracing", +] + +[[package]] +name = "toml" +version = "0.5.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8d82e1a7758622a465f8cee077614c73484dac5b836c02ff6a40d5d1010324d7" +dependencies = [ + "serde", +] + +[[package]] +name = "tower-service" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6bc1c9ce2b5135ac7f93c72918fc37feb872bdc6a5533a8b85eb4b86bfdae52" + +[[package]] +name = "tracing" +version = "0.1.37" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ce8c33a8d48bd45d624a6e523445fd21ec13d3653cd51f681abf67418f54eb8" +dependencies = [ + "cfg-if", + "log", + "pin-project-lite", + "tracing-attributes", + "tracing-core", +] + +[[package]] +name = "tracing-actix-web" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d725b8fa6ef307b3f4856913523337de45c47cc79271bafd7acfb39559e3a2da" +dependencies = [ + "actix-web", + "pin-project", + "tracing", + "uuid 1.2.2", +] + +[[package]] +name = "tracing-attributes" +version = "0.1.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4017f8f45139870ca7e672686113917c71c7a6e02d4924eda67186083c03081a" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "tracing-core" +version = "0.1.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24eb03ba0eab1fd845050058ce5e616558e8f8d8fca633e6b163fe25c797213a" +dependencies = [ + "once_cell", +] + +[[package]] +name = "try-lock" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "59547bce71d9c38b83d9c0e92b6066c4253371f15005def0c30d9657f50c7642" + +[[package]] +name = "typenum" +version = "1.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dcf81ac59edc17cc8697ff311e8f5ef2d99fcbd9817b34cec66f90b6c3dfd987" + +[[package]] +name = "ucd-trie" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e79c4d996edb816c91e4308506774452e55e95c3c9de07b6729e17e15a5ef81" + +[[package]] +name = "unic-char-property" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a8c57a407d9b6fa02b4795eb81c5b6652060a15a7903ea981f3d723e6c0be221" +dependencies = [ + "unic-char-range", +] + +[[package]] +name = "unic-char-range" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0398022d5f700414f6b899e10b8348231abf9173fa93144cbc1a43b9793c1fbc" + +[[package]] +name = "unic-common" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "80d7ff825a6a654ee85a63e80f92f054f904f21e7d12da4e22f9834a4aaa35bc" + +[[package]] +name = "unic-segment" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e4ed5d26be57f84f176157270c112ef57b86debac9cd21daaabbe56db0f88f23" +dependencies = [ + "unic-ucd-segment", +] + +[[package]] +name = "unic-ucd-segment" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2079c122a62205b421f499da10f3ee0f7697f012f55b675e002483c73ea34700" +dependencies = [ + "unic-char-property", + "unic-char-range", + "unic-ucd-version", +] + +[[package]] +name = "unic-ucd-version" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96bd2f2237fe450fcd0a1d2f5f4e91711124f7857ba2e964247776ebeeb7b0c4" +dependencies = [ + "unic-common", +] + +[[package]] +name = "unicase" +version = "2.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "50f37be617794602aabbeee0be4f259dc1778fabe05e2d67ee8f79326d5cb4f6" +dependencies = [ + "version_check", +] + +[[package]] +name = "unicode-bidi" +version = "0.3.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "099b7128301d285f79ddd55b9a83d5e6b9e97c92e0ea0daebee7263e932de992" + +[[package]] +name = "unicode-ident" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ceab39d59e4c9499d4e5a8ee0e2735b891bb7308ac83dfb4e80cad195c9f6f3" + +[[package]] +name = "unicode-normalization" +version = "0.1.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c5713f0fc4b5db668a2ac63cdb7bb4469d8c9fed047b1d0292cc7b0ce2ba921" +dependencies = [ + "tinyvec", +] + +[[package]] +name = "unicode-segmentation" +version = "1.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fdbf052a0783de01e944a6ce7a8cb939e295b1e7be835a1112c3b9a7f047a5a" + +[[package]] +name = "unicode-width" +version = "0.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c0edd1e5b14653f783770bce4a4dabb4a5108a5370a5f5d8cfe8710c361f6c8b" + +[[package]] +name = "unicode_categories" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "39ec24b3121d976906ece63c9daad25b85969647682eee313cb5779fdd69e14e" + +[[package]] +name = "universal-hash" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7d3160b73c9a19f7e2939a2fdad446c57c1bbbbf4d919d3213ff1267a580d8b5" +dependencies = [ + "crypto-common", + "subtle", +] + +[[package]] +name = "unsafe-libyaml" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c1e5fa573d8ac5f1a856f8d7be41d390ee973daf97c806b2c1a465e4e1406e68" + +[[package]] +name = "untrusted" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a156c684c91ea7d62626509bce3cb4e1d9ed5c4d978f7b4352658f96a4c26b4a" + +[[package]] +name = "url" +version = "2.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0d68c799ae75762b8c3fe375feb6600ef5602c883c5d21eb51c09f22b83c4643" +dependencies = [ + "form_urlencoded", + "idna 0.3.0", + "percent-encoding", + "serde", +] + +[[package]] +name = "urlencoding" +version = "2.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e8db7427f936968176eaa7cdf81b7f98b980b18495ec28f1b5791ac3bfe3eea9" + +[[package]] +name = "utf-8" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09cc8ee72d2a9becf2f2febe0205bbed8fc6615b7cb429ad062dc7b7ddd036a9" + +[[package]] +name = "uuid" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bc5cf98d8186244414c848017f0e2676b3fcb46807f6668a97dfe67359a3c4b7" +dependencies = [ + "getrandom 0.2.8", +] + +[[package]] +name = "uuid" +version = "1.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "422ee0de9031b5b948b97a8fc04e3aa35230001a722ddd27943e0be31564ce4c" +dependencies = [ + "getrandom 0.2.8", + "serde", +] + +[[package]] +name = "validator" +version = "0.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f07b0a1390e01c0fc35ebb26b28ced33c9a3808f7f9fbe94d3cc01e233bfeed5" +dependencies = [ + "idna 0.2.3", + "lazy_static", + "regex", + "serde", + "serde_derive", + "serde_json", + "url", + "validator_derive", +] + +[[package]] +name = "validator_derive" +version = "0.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ea7ed5e8cf2b6bdd64a6c4ce851da25388a89327b17b88424ceced6bd5017923" +dependencies = [ + "if_chain", + "lazy_static", + "proc-macro-error", + "proc-macro2", + "quote", + "regex", + "syn", + "validator_types", +] + +[[package]] +name = "validator_types" +version = "0.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d2ddf34293296847abfc1493b15c6e2f5d3cd19f57ad7d22673bf4c6278da329" +dependencies = [ + "proc-macro2", + "syn", +] + +[[package]] +name = "vcpkg" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" + +[[package]] +name = "version_check" +version = "0.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f" + +[[package]] +name = "walkdir" +version = "2.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "808cf2735cd4b6866113f648b791c6adc5714537bc222d9347bb203386ffda56" +dependencies = [ + "same-file", + "winapi", + "winapi-util", +] + +[[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.11.0+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" + +[[package]] +name = "wasm-bindgen" +version = "0.2.83" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eaf9f5aceeec8be17c128b2e93e031fb8a4d469bb9c4ae2d7dc1888b26887268" +dependencies = [ + "cfg-if", + "wasm-bindgen-macro", +] + +[[package]] +name = "wasm-bindgen-backend" +version = "0.2.83" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4c8ffb332579b0557b52d268b91feab8df3615f265d5270fec2a8c95b17c1142" +dependencies = [ + "bumpalo", + "log", + "once_cell", + "proc-macro2", + "quote", + "syn", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-futures" +version = "0.4.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "23639446165ca5a5de86ae1d8896b737ae80319560fbaa4c2887b7da6e7ebd7d" +dependencies = [ + "cfg-if", + "js-sys", + "wasm-bindgen", + "web-sys", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.83" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "052be0f94026e6cbc75cdefc9bae13fd6052cdcaf532fa6c45e7ae33a1e6c810" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.83" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "07bc0c051dc5f23e307b13285f9d75df86bfdf816c5721e573dec1f9b8aa193c" +dependencies = [ + "proc-macro2", + "quote", + "syn", + "wasm-bindgen-backend", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.83" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1c38c045535d93ec4f0b4defec448e4291638ee608530863b1e2ba115d4fff7f" + +[[package]] +name = "web-sys" +version = "0.3.60" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bcda906d8be16e728fd5adc5b729afad4e444e106ab28cd1c7256e54fa61510f" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "webpki" +version = "0.22.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f095d78192e208183081cc07bc5515ef55216397af48b873e5edcd72637fa1bd" +dependencies = [ + "ring", + "untrusted", +] + +[[package]] +name = "webpki-roots" +version = "0.22.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "368bfe657969fb01238bb756d351dcade285e0f6fcbd36dcb23359a5169975be" +dependencies = [ + "webpki", +] + +[[package]] +name = "winapi" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" +dependencies = [ + "winapi-i686-pc-windows-gnu", + "winapi-x86_64-pc-windows-gnu", +] + +[[package]] +name = "winapi-i686-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" + +[[package]] +name = "winapi-util" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "70ec6ce85bb158151cae5e5c87f95a8e97d2c0c4b001223f33a334e3ce5de178" +dependencies = [ + "winapi", +] + +[[package]] +name = "winapi-x86_64-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" + +[[package]] +name = "windows-sys" +version = "0.36.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ea04155a16a59f9eab786fe12a4a450e75cdb175f9e0d80da1e17db09f55b8d2" +dependencies = [ + "windows_aarch64_msvc 0.36.1", + "windows_i686_gnu 0.36.1", + "windows_i686_msvc 0.36.1", + "windows_x86_64_gnu 0.36.1", + "windows_x86_64_msvc 0.36.1", +] + +[[package]] +name = "windows-sys" +version = "0.42.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a3e1820f08b8513f676f7ab6c1f99ff312fb97b553d30ff4dd86f9f15728aa7" +dependencies = [ + "windows_aarch64_gnullvm", + "windows_aarch64_msvc 0.42.0", + "windows_i686_gnu 0.42.0", + "windows_i686_msvc 0.42.0", + "windows_x86_64_gnu 0.42.0", + "windows_x86_64_gnullvm", + "windows_x86_64_msvc 0.42.0", +] + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.42.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41d2aa71f6f0cbe00ae5167d90ef3cfe66527d6f613ca78ac8024c3ccab9a19e" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.36.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9bb8c3fd39ade2d67e9874ac4f3db21f0d710bee00fe7cab16949ec184eeaa47" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.42.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dd0f252f5a35cac83d6311b2e795981f5ee6e67eb1f9a7f64eb4500fbc4dcdb4" + +[[package]] +name = "windows_i686_gnu" +version = "0.36.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "180e6ccf01daf4c426b846dfc66db1fc518f074baa793aa7d9b9aaeffad6a3b6" + +[[package]] +name = "windows_i686_gnu" +version = "0.42.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fbeae19f6716841636c28d695375df17562ca208b2b7d0dc47635a50ae6c5de7" + +[[package]] +name = "windows_i686_msvc" +version = "0.36.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e2e7917148b2812d1eeafaeb22a97e4813dfa60a3f8f78ebe204bcc88f12f024" + +[[package]] +name = "windows_i686_msvc" +version = "0.42.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "84c12f65daa39dd2babe6e442988fc329d6243fdce47d7d2d155b8d874862246" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.36.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4dcd171b8776c41b97521e5da127a2d86ad280114807d0b2ab1e462bc764d9e1" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.42.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bf7b1b21b5362cbc318f686150e5bcea75ecedc74dd157d874d754a2ca44b0ed" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.42.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09d525d2ba30eeb3297665bd434a54297e4170c7f1a44cad4ef58095b4cd2028" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.36.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c811ca4a8c853ef420abd8592ba53ddbbac90410fab6903b3e79972a631f7680" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.42.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f40009d85759725a34da6d89a94e63d7bdc50a862acf0dbc7c8e488f1edcb6f5" + +[[package]] +name = "winreg" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "80d0f4e272c85def139476380b12f9ac60926689dd2e01d4923222f40580869d" +dependencies = [ + "winapi", +] + +[[package]] +name = "yaml-rust" +version = "0.4.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56c1936c4cc7a1c9ab21a1ebb602eb942ba868cbd44a99cb7cdc5892335e1c85" +dependencies = [ + "linked-hash-map", +] + +[[package]] +name = "zstd" +version = "0.11.2+zstd.1.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "20cc960326ece64f010d2d2107537f26dc589a6573a316bd5b1dba685fa5fde4" +dependencies = [ + "zstd-safe", +] + +[[package]] +name = "zstd-safe" +version = "5.0.2+zstd.1.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d2a5585e04f9eea4b2a3d1eca508c4dee9592a89ef6f450c11719da0726f4db" +dependencies = [ + "libc", + "zstd-sys", +] + +[[package]] +name = "zstd-sys" +version = "2.0.4+zstd.1.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4fa202f2ef00074143e219d15b62ffc317d17cc33909feac471c044087cad7b0" +dependencies = [ + "cc", + "libc", +] diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..845d2d3 --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,104 @@ +[package] +name = "pativu" +version = "0.1.0" +edition = "2021" +repository = "https://git.batsense.net/realaravinth/pativu" +homepage = "https://git.batsense.net/realaravinth/pativu" +documentation = "https://git.batsense.net/realaravinth/pativu" +readme = "README.md" +license = "AGPLv3 or later version" +authors = ["realaravinth "] +build = "build.rs" + + + +#[dependencies] +#actix-web = "4" +#futures-util = { version = "0.3.17", default-features = false, features = ["std"] } +#lazy_static = "1.4.0" +#log = "0.4.17" +#pretty_env_logger = "0.4.0" +#serde = { version = "1", features=["derive"]} +#actix-web-codegen-const-routes = { version = "0.1.0", tag = "0.1.0", git = "https://github.com/realaravinth/actix-web-codegen-const-routes" } +#derive_builder = "0.11.2" +#config = "0.11" +#derive_more = "0.99.17" +#url = { version = "2.2.2", features = ["serde"]} +#serde_json = { version ="1", features = ["raw_value"]} +#clap = { vesrion = "3.2.20", features = ["derive"]} + +[build-dependencies] +serde_json = "1" +sqlx = { version = "0.6.1", features = [ "runtime-actix-rustls", "sqlite", "time", "offline"] } + +#[dev-dependencies] +#actix-rt = "2.7.0" +#base64 = "0.13.0" + +[dependencies] +actix-web = "4.0.1" +actix-http = "3.0.4" +actix-identity = "0.4.0" +actix-rt = "2" +actix-web-codegen-const-routes = { version = "0.1.0", tag = "0.1.0", git = "https://github.com/realaravinth/actix-web-codegen-const-routes" } +argon2-creds = { branch = "master", git = "https://github.com/realaravinth/argon2-creds"} +sqlx = { version = "0.6.2", features = ["sqlite", "time", "offline", "runtime-actix-rustls", "uuid", "json"] } +clap = { version = "3.2.20", features = ["derive"]} + +config = "0.13" + +serde = { version = "1", features = ["derive", "rc"]} +serde_json = "1" + +pretty_env_logger = "0.4" + +lazy_static = "1.4" +url = { version = "2.2", features = ["serde"] } +urlencoding = "2.1.0" + +derive_more = "0.99" + +num_cpus = "1.13" + +tokio = { version = "1", features=["sync", "fs"]} +num_enum = "0.5.7" + +mime_guess = "2.0.4" +mime = "0.3.16" +rust-embed = "6.3.0" +rand = "0.8.5" +tracing = { version = "0.1.37", features = ["log"]} +tracing-actix-web = "0.6.2" +toml = "0.5.9" +serde_yaml = "0.9.14" +uuid = { version = "1.2.2", features = ["serde"] } +reqwest = { version = "0.11.13", features = ["rustls-tls-native-roots", "stream", "gzip", "deflate", "brotli", "json"]} +futures-util = "0.3.25" +lol_html = "0.3.1" +scraper = "0.13.0" + + + + + +[dependencies.cache-buster] +git = "https://github.com/realaravinth/cache-buster" + +[dependencies.tera] +default-features = false +version = "1.15.0" + +[dependencies.actix-auth-middleware] +branch = "v4" +features = ["actix_identity_backend"] +git = "https://github.com/realaravinth/actix-auth-middleware" +version = "0.2" + + +[dev-dependencies] +futures = "0.3.24" +mktemp = "0.4.1" + + +[workspace] +exclude = ["utils/cache-bust"] diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..f20e6ea --- /dev/null +++ b/Makefile @@ -0,0 +1,72 @@ +define cache_bust ## run cache_busting program + npm run sass + cd utils/cache-bust && cargo run +endef + +default: ## Debug build + $(call cache_bust) + cargo run -- serve + +cache-bust: ## Run cache buster on static assets + $(call cache_bust) + +check: ## Check for syntax errors on all workspaces + cargo check --workspace --tests --all-features + #cd utils/cache-bust && cargo check --tests --all-features + +clean: ## Clean all build artifacts and dependencies + @cargo clean + +coverage: ## Generate HTML code coverage + $(call cache_bust) + cargo tarpaulin -t 1200 --out Html + +dev-env: ## Download development dependencies + npm install + cargo fetch + +doc: ## Prepare documentation + cargo doc --no-deps --workspace --all-features + +docker: ## Build docker images + docker build \ + -t realaravinth/pativu:master \ + -t realaravinth/pativu:latest \ + -t realaravinth/pativu:0.1.0 . + +docker-publish: docker ## Build and publish docker images + docker push realaravinth/pativu:master + docker push realaravinth/pativu:latest + docker push realaravinth/pativu:0.1.0 + +lint: ## Lint codebase + cargo fmt -v --all -- --emit files + cargo clippy --workspace --tests --all-features + +migrate: ## run migrations + $(call cache_bust) + unset DATABASE_URL && cargo build + cargo run -- migrate + +release: ## Release build + $(call cache_bust) + cargo build --release + +run: default ## Run debug build + cargo run -- serve + +sqlx-offline-data: ## prepare sqlx offline data + cargo sqlx prepare \ + --database-url=${DATABASE_URL} -- \ + --all-features + +test: ## Run tests + $(call cache_bust) + cargo test --all-features --no-fail-fast + +xml-test-coverage: ## Generate cobertura.xml test coverage + $(call cache_bust) + cargo tarpaulin -t 1200 --out Xml + +help: ## Prints help for targets with comments + @cat $(MAKEFILE_LIST) | grep -E '^[a-zA-Z_-]+:.*?## .*$$' | awk 'BEGIN {FS = ":.*?## "}; {printf "\033[36m%-30s\033[0m %s\n", $$1, $$2}' diff --git a/README.md b/README.md new file mode 100644 index 0000000..4873001 --- /dev/null +++ b/README.md @@ -0,0 +1,22 @@ +[![Docker](https://img.shields.io/docker/pulls/realaravinth/librepages-conductor)](https://hub.docker.com/r/realaravinth/librepages-conductor) +[![status-badge](https://ci.batsense.net/api/badges/LibrePages/conductor/status.svg)](https://ci.batsense.net/LibrePages/conductor) + + +
+

Conductor

+

+

+ +Enabling custom domain support(configuring reverse proxy, deploying TLS +certificates, etc.) are environtment dependent. LibrePages will notify +conductor when a new hostname should be deployed. + +## Launch docker container + + +```bash +docker run -p 7000:7000 \ + -v $(pwd)/config/:/etc/lpconductor/ \ + -e LPCONDUCTOR_CONFIG=/etc/lpconductor/default.toml \ + realaravinth/librepages-conductor:latest +``` diff --git a/build.rs b/build.rs new file mode 100644 index 0000000..f959f74 --- /dev/null +++ b/build.rs @@ -0,0 +1,26 @@ +/* + * Copyright (C) 2021 Aravinth Manivannan + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +use std::process::Command; + +fn main() { + let output = Command::new("git") + .args(["rev-parse", "HEAD"]) + .output() + .expect("error in git command, is git installed?"); + let git_hash = String::from_utf8(output.stdout).unwrap(); + println!("cargo:rustc-env=GIT_HASH={}", git_hash); +} diff --git a/config/default.toml b/config/default.toml new file mode 100644 index 0000000..cad4999 --- /dev/null +++ b/config/default.toml @@ -0,0 +1,37 @@ +debug = true +allow_registration = true +# source code of your copy of pages server. +source_code = "https://git.batsense.net/realaravinth/pativu" +support_email = "support@librepages.example.org" + +[server] +# The port at which you want Pages to listen to +port = 7000 +#IP address. Enter 0.0.0.0 to listen on all availale addresses +ip= "0.0.0.0" +# The number of worker threads that must be spun up by the Pages server. +# Minimum of two threads are advisable for top async performance but can work +# with one also. +workers = 2 +domain = "localhost" +cookie_secret = "94b2b2732626fdb7736229a7c777cb451e6304c147c4549f30" + + +[pages] +base_path = "/tmp/pativu-defualt-config/" + +[database] +# This section deals with the database location and how to access it +# Please note that at the moment, we have support for only sqlite. +# Example, if you are Batman, your config would be: +# hostname = "batcave.org" +# port = "5432" +# username = "batman" +# password = "somereallycomplicatedBatmanpassword" +hostname = "localhost" +port = "5432" +username = "sqlite" +password = "password" +name = "sqlite" +pool = 4 +database_type="sqlite" diff --git a/migrations/20220910140647_librepages_users.sql b/migrations/20220910140647_librepages_users.sql new file mode 100644 index 0000000..c1dc4a1 --- /dev/null +++ b/migrations/20220910140647_librepages_users.sql @@ -0,0 +1,7 @@ +CREATE TABLE IF NOT EXISTS librepages_users ( + name VARCHAR(100) NOT NULL UNIQUE, + email VARCHAR(100) UNIQUE NOT NULL, + email_verified BOOLEAN DEFAULT NULL, + password TEXT NOT NULL, + ID INTEGER PRIMARY KEY NOT NULL +); diff --git a/migrations/20220921122103_librepages_sites.sql b/migrations/20220921122103_librepages_sites.sql new file mode 100644 index 0000000..ad8f1dc --- /dev/null +++ b/migrations/20220921122103_librepages_sites.sql @@ -0,0 +1,5 @@ +CREATE TABLE IF NOT EXISTS librepages_sites ( + url VARCHAR(3000) NOT NULL UNIQUE, + ID INTEGER PRIMARY KEY NOT NULL, + owned_by INTEGER NOT NULL references librepages_users(ID) ON DELETE CASCADE +); diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000..8f3e0b2 --- /dev/null +++ b/package-lock.json @@ -0,0 +1,385 @@ +{ + "name": "librepages", + "version": "1.0.0", + "lockfileVersion": 2, + "requires": true, + "packages": { + "": { + "name": "librepages", + "version": "1.0.0", + "license": "ISC", + "devDependencies": { + "sass": "^1.54.9" + } + }, + "node_modules/anymatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.2.tgz", + "integrity": "sha512-P43ePfOAIupkguHUycrc4qJ9kz8ZiuOUijaETwX7THt0Y/GNK7v0aa8rY816xWjZ7rJdA5XdMcpVFTKMq+RvWg==", + "dev": true, + "dependencies": { + "normalize-path": "^3.0.0", + "picomatch": "^2.0.4" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/binary-extensions": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.2.0.tgz", + "integrity": "sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/braces": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz", + "integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==", + "dev": true, + "dependencies": { + "fill-range": "^7.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/chokidar": { + "version": "3.5.3", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.5.3.tgz", + "integrity": "sha512-Dr3sfKRP6oTcjf2JmUmFJfeVMvXBdegxB0iVQ5eb2V10uFJUCAS8OByZdVAyVb8xXNz3GjjTgj9kLWsZTqE6kw==", + "dev": true, + "funding": [ + { + "type": "individual", + "url": "https://paulmillr.com/funding/" + } + ], + "dependencies": { + "anymatch": "~3.1.2", + "braces": "~3.0.2", + "glob-parent": "~5.1.2", + "is-binary-path": "~2.1.0", + "is-glob": "~4.0.1", + "normalize-path": "~3.0.0", + "readdirp": "~3.6.0" + }, + "engines": { + "node": ">= 8.10.0" + }, + "optionalDependencies": { + "fsevents": "~2.3.2" + } + }, + "node_modules/fill-range": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz", + "integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==", + "dev": true, + "dependencies": { + "to-regex-range": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/fsevents": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz", + "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==", + "dev": true, + "hasInstallScript": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/immutable": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/immutable/-/immutable-4.1.0.tgz", + "integrity": "sha512-oNkuqVTA8jqG1Q6c+UglTOD1xhC1BtjKI7XkCXRkZHrN5m18/XsnUp8Q89GkQO/z+0WjonSvl0FLhDYftp46nQ==", + "dev": true + }, + "node_modules/is-binary-path": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", + "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", + "dev": true, + "dependencies": { + "binary-extensions": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "dev": true, + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "dev": true, + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/normalize-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", + "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/picomatch": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "dev": true, + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/readdirp": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", + "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", + "dev": true, + "dependencies": { + "picomatch": "^2.2.1" + }, + "engines": { + "node": ">=8.10.0" + } + }, + "node_modules/sass": { + "version": "1.54.9", + "resolved": "https://registry.npmjs.org/sass/-/sass-1.54.9.tgz", + "integrity": "sha512-xb1hjASzEH+0L0WI9oFjqhRi51t/gagWnxLiwUNMltA0Ab6jIDkAacgKiGYKM9Jhy109osM7woEEai6SXeJo5Q==", + "dev": true, + "dependencies": { + "chokidar": ">=3.0.0 <4.0.0", + "immutable": "^4.0.0", + "source-map-js": ">=0.6.2 <2.0.0" + }, + "bin": { + "sass": "sass.js" + }, + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/source-map-js": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.0.2.tgz", + "integrity": "sha512-R0XvVJ9WusLiqTCEiGCmICCMplcCkIwwR11mOSD9CR5u+IXYdiseeEuXCVAjS54zqwkLcPNnmU4OeJ6tUrWhDw==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "dev": true, + "dependencies": { + "is-number": "^7.0.0" + }, + "engines": { + "node": ">=8.0" + } + } + }, + "dependencies": { + "anymatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.2.tgz", + "integrity": "sha512-P43ePfOAIupkguHUycrc4qJ9kz8ZiuOUijaETwX7THt0Y/GNK7v0aa8rY816xWjZ7rJdA5XdMcpVFTKMq+RvWg==", + "dev": true, + "requires": { + "normalize-path": "^3.0.0", + "picomatch": "^2.0.4" + } + }, + "binary-extensions": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.2.0.tgz", + "integrity": "sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA==", + "dev": true + }, + "braces": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz", + "integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==", + "dev": true, + "requires": { + "fill-range": "^7.0.1" + } + }, + "chokidar": { + "version": "3.5.3", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.5.3.tgz", + "integrity": "sha512-Dr3sfKRP6oTcjf2JmUmFJfeVMvXBdegxB0iVQ5eb2V10uFJUCAS8OByZdVAyVb8xXNz3GjjTgj9kLWsZTqE6kw==", + "dev": true, + "requires": { + "anymatch": "~3.1.2", + "braces": "~3.0.2", + "fsevents": "~2.3.2", + "glob-parent": "~5.1.2", + "is-binary-path": "~2.1.0", + "is-glob": "~4.0.1", + "normalize-path": "~3.0.0", + "readdirp": "~3.6.0" + } + }, + "fill-range": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz", + "integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==", + "dev": true, + "requires": { + "to-regex-range": "^5.0.1" + } + }, + "fsevents": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz", + "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==", + "dev": true, + "optional": true + }, + "glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "requires": { + "is-glob": "^4.0.1" + } + }, + "immutable": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/immutable/-/immutable-4.1.0.tgz", + "integrity": "sha512-oNkuqVTA8jqG1Q6c+UglTOD1xhC1BtjKI7XkCXRkZHrN5m18/XsnUp8Q89GkQO/z+0WjonSvl0FLhDYftp46nQ==", + "dev": true + }, + "is-binary-path": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", + "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", + "dev": true, + "requires": { + "binary-extensions": "^2.0.0" + } + }, + "is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "dev": true + }, + "is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "dev": true, + "requires": { + "is-extglob": "^2.1.1" + } + }, + "is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "dev": true + }, + "normalize-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", + "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", + "dev": true + }, + "picomatch": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "dev": true + }, + "readdirp": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", + "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", + "dev": true, + "requires": { + "picomatch": "^2.2.1" + } + }, + "sass": { + "version": "1.54.9", + "resolved": "https://registry.npmjs.org/sass/-/sass-1.54.9.tgz", + "integrity": "sha512-xb1hjASzEH+0L0WI9oFjqhRi51t/gagWnxLiwUNMltA0Ab6jIDkAacgKiGYKM9Jhy109osM7woEEai6SXeJo5Q==", + "dev": true, + "requires": { + "chokidar": ">=3.0.0 <4.0.0", + "immutable": "^4.0.0", + "source-map-js": ">=0.6.2 <2.0.0" + } + }, + "source-map-js": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.0.2.tgz", + "integrity": "sha512-R0XvVJ9WusLiqTCEiGCmICCMplcCkIwwR11mOSD9CR5u+IXYdiseeEuXCVAjS54zqwkLcPNnmU4OeJ6tUrWhDw==", + "dev": true + }, + "to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "dev": true, + "requires": { + "is-number": "^7.0.0" + } + } + } +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..670bd04 --- /dev/null +++ b/package.json @@ -0,0 +1,25 @@ +{ + "name": "librepages", + "version": "1.0.0", + "description": "

Pages

", + "main": "index.js", + "directories": { + "doc": "docs" + }, + "scripts": { + "sass": "rm -rf static/cache/css/*.css && sass templates/main.scss static/cache/css/main.css && sass templates/mobile.scss static/cache/css/mobile.css" + }, + "repository": { + "type": "git", + "url": "git+https://github.com/realaravinth/librepages.git" + }, + "author": "", + "license": "ISC", + "bugs": { + "url": "https://github.com/realaravinth/librepages/issues" + }, + "homepage": "https://github.com/realaravinth/librepages#readme", + "devDependencies": { + "sass": "^1.54.9" + } +} diff --git a/sqlx-data.json b/sqlx-data.json new file mode 100644 index 0000000..93f7035 --- /dev/null +++ b/sqlx-data.json @@ -0,0 +1,3 @@ +{ + "db": "SQLite" +} \ No newline at end of file diff --git a/src/api/mod.rs b/src/api/mod.rs new file mode 100644 index 0000000..5aa8f74 --- /dev/null +++ b/src/api/mod.rs @@ -0,0 +1,17 @@ +/* + * Copyright (C) 2022 Aravinth Manivannan + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +pub mod v1; diff --git a/src/api/v1/account/mod.rs b/src/api/v1/account/mod.rs new file mode 100644 index 0000000..c2716e2 --- /dev/null +++ b/src/api/v1/account/mod.rs @@ -0,0 +1,144 @@ +/* + * Copyright (C) 2021 Aravinth Manivannan + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +use actix_identity::Identity; +use actix_web::{web, HttpResponse, Responder}; +use serde::{Deserialize, Serialize}; + +use crate::ctx::api::v1::account::*; +use crate::ctx::api::v1::auth::Password; +use crate::errors::*; +use crate::AppCtx; + +#[cfg(test)] +pub mod test; + +pub use super::auth; + +#[derive(Clone, Debug, Deserialize, Serialize)] +pub struct AccountCheckPayload { + pub val: String, +} + +pub fn services(cfg: &mut actix_web::web::ServiceConfig) { + cfg.service(username_exists); + cfg.service(set_username); + cfg.service(email_exists); + cfg.service(set_email); + cfg.service(delete_account); + cfg.service(update_user_password); +} + +#[derive(Clone, Debug, Deserialize, Serialize)] +pub struct Email { + pub email: String, +} + +#[derive(Clone, Debug, Deserialize, Serialize)] +pub struct Username { + pub username: String, +} + +/// update username +#[actix_web_codegen_const_routes::post( + path = "crate::V1_API_ROUTES.account.update_username", + wrap = "super::get_auth_middleware()" +)] +#[tracing::instrument(name = "Update username", skip(ctx, payload, id))] +async fn set_username( + id: Identity, + payload: web::Json, + ctx: AppCtx, +) -> ServiceResult { + let username = id.identity().unwrap(); + + let new_name = ctx.update_username(&username, &payload.username).await?; + + id.forget(); + id.remember(new_name); + + Ok(HttpResponse::Ok()) +} + +#[actix_web_codegen_const_routes::post(path = "crate::V1_API_ROUTES.account.username_exists")] +#[tracing::instrument(name = "Check if username exists", skip(ctx, payload))] +async fn username_exists( + payload: web::Json, + ctx: AppCtx, +) -> ServiceResult { + Ok(HttpResponse::Ok().json(ctx.username_exists(&payload.val).await?)) +} + +#[actix_web_codegen_const_routes::post(path = "crate::V1_API_ROUTES.account.email_exists")] +#[tracing::instrument(name = "Check if email exists", skip(ctx, payload))] +pub async fn email_exists( + payload: web::Json, + ctx: AppCtx, +) -> ServiceResult { + Ok(HttpResponse::Ok().json(ctx.email_exists(&payload.val).await?)) +} + +/// update email +#[actix_web_codegen_const_routes::post( + path = "crate::V1_API_ROUTES.account.update_email", + wrap = "super::get_auth_middleware()" +)] +#[tracing::instrument(name = "Update email", skip(ctx, payload, id))] +async fn set_email( + id: Identity, + payload: web::Json, + ctx: AppCtx, +) -> ServiceResult { + let username = id.identity().unwrap(); + ctx.set_email(&username, &payload.email).await?; + Ok(HttpResponse::Ok()) +} + +#[actix_web_codegen_const_routes::post( + path = "crate::V1_API_ROUTES.account.delete", + wrap = "super::get_auth_middleware()" +)] +#[tracing::instrument(name = "Delete account", skip(ctx, payload, id))] +async fn delete_account( + id: Identity, + payload: web::Json, + ctx: AppCtx, +) -> ServiceResult { + let username = id.identity().unwrap(); + + ctx.delete_user(&username, &payload.password).await?; + id.forget(); + Ok(HttpResponse::Ok()) +} + +#[actix_web_codegen_const_routes::post( + path = "crate::V1_API_ROUTES.account.update_password", + wrap = "super::get_auth_middleware()" +)] +#[tracing::instrument(name = "Update user password", skip(ctx, payload, id))] +async fn update_user_password( + id: Identity, + ctx: AppCtx, + + payload: web::Json, +) -> ServiceResult { + let username = id.identity().unwrap(); + let payload = payload.into_inner(); + ctx.change_password(&username, &payload).await?; + + Ok(HttpResponse::Ok()) +} diff --git a/src/api/v1/account/test.rs b/src/api/v1/account/test.rs new file mode 100644 index 0000000..29382e0 --- /dev/null +++ b/src/api/v1/account/test.rs @@ -0,0 +1,296 @@ +/* +* Copyright (C) 2021 Aravinth Manivannan +* +* This program is free software: you can redistribute it and/or modify +* it under the terms of the GNU Affero General Public License as +* published by the Free Software Foundation, either version 3 of the +* License, or (at your option) any later version. +* +* This program is distributed in the hope that it will be useful, +* but WITHOUT ANY WARRANTY; without even the implied warranty of +* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +* GNU Affero General Public License for more details. +* +* You should have received a copy of the GNU Affero General Public License +* along with this program. If not, see . +*/ +use std::sync::Arc; + +use actix_web::http::StatusCode; +use actix_web::test; + +use super::*; +use crate::api::v1::ROUTES; +use crate::ctx::api::v1::auth::Password; +use crate::ctx::Ctx; +use crate::*; + +#[actix_rt::test] +async fn postgrest_account_works() { + let (_, ctx) = crate::tests::get_ctx().await; + uname_email_exists_works(ctx.clone()).await; + email_udpate_password_validation_del_userworks(ctx.clone()).await; + username_update_works(ctx.clone()).await; + update_password_works(ctx.clone()).await; +} + +async fn uname_email_exists_works(ctx: Arc) { + const NAME: &str = "testuserexists"; + const PASSWORD: &str = "longpasswordasdfa2"; + const EMAIL: &str = "testuserexists2@a.com"; + + let _ = ctx.delete_user(NAME, PASSWORD).await; + + let (_, signin_resp) = ctx.register_and_signin(NAME, EMAIL, PASSWORD).await; + let cookies = get_cookie!(signin_resp); + let app = get_app!(ctx).await; + + let mut payload = AccountCheckPayload { val: NAME.into() }; + + let user_exists_resp = test::call_service( + &app, + post_request!(&payload, ROUTES.account.username_exists) + .cookie(cookies.clone()) + .to_request(), + ) + .await; + assert_eq!(user_exists_resp.status(), StatusCode::OK); + let mut resp: AccountCheckResp = test::read_body_json(user_exists_resp).await; + assert!(resp.exists); + + payload.val = PASSWORD.into(); + + let user_doesnt_exist = test::call_service( + &app, + post_request!(&payload, ROUTES.account.username_exists) + .cookie(cookies.clone()) + .to_request(), + ) + .await; + assert_eq!(user_doesnt_exist.status(), StatusCode::OK); + resp = test::read_body_json(user_doesnt_exist).await; + assert!(!resp.exists); + + let email_doesnt_exist = test::call_service( + &app, + post_request!(&payload, ROUTES.account.email_exists) + .cookie(cookies.clone()) + .to_request(), + ) + .await; + assert_eq!(email_doesnt_exist.status(), StatusCode::OK); + resp = test::read_body_json(email_doesnt_exist).await; + assert!(!resp.exists); + + payload.val = EMAIL.into(); + + let email_exist = test::call_service( + &app, + post_request!(&payload, ROUTES.account.email_exists) + .cookie(cookies.clone()) + .to_request(), + ) + .await; + assert_eq!(email_exist.status(), StatusCode::OK); + resp = test::read_body_json(email_exist).await; + assert!(resp.exists); +} + +async fn email_udpate_password_validation_del_userworks(ctx: Arc) { + const NAME: &str = "testuser2"; + const PASSWORD: &str = "longpassword2"; + const EMAIL: &str = "testuser1@a.com2"; + const NAME2: &str = "eupdauser"; + const EMAIL2: &str = "eupdauser@a.com"; + + let _ = ctx.delete_user(NAME, PASSWORD).await; + let _ = ctx.delete_user(NAME2, PASSWORD).await; + + let _ = ctx.register_and_signin(NAME2, EMAIL2, PASSWORD).await; + let (_creds, signin_resp) = ctx.register_and_signin(NAME, EMAIL, PASSWORD).await; + let cookies = get_cookie!(signin_resp); + let app = get_app!(ctx).await; + + // update email + let mut email_payload = Email { + email: EMAIL.into(), + }; + let email_update_resp = test::call_service( + &app, + post_request!(&email_payload, ROUTES.account.update_email) + //post_request!(&email_payload, EMAIL_UPDATE) + .cookie(cookies.clone()) + .to_request(), + ) + .await; + assert_eq!(email_update_resp.status(), StatusCode::OK); + + // check duplicate email while duplicate email + email_payload.email = EMAIL2.into(); + // ctx.bad_post_req_test( + // NAME, + // PASSWORD, + // ROUTES.account.update_email, + // &email_payload, + // ServiceError::EmailTaken, + // ) + // .await; + + // wrong password while deleteing account + let mut payload = Password { + password: NAME.into(), + }; + ctx.bad_post_req_test( + NAME, + PASSWORD, + ROUTES.account.delete, + &payload, + ServiceError::WrongPassword, + ) + .await; + + // delete account + payload.password = PASSWORD.into(); + let delete_user_resp = test::call_service( + &app, + post_request!(&payload, ROUTES.account.delete) + .cookie(cookies.clone()) + .to_request(), + ) + .await; + + assert_eq!(delete_user_resp.status(), StatusCode::OK); + + // try to delete an account that doesn't exist + let account_not_found_resp = test::call_service( + &app, + post_request!(&payload, ROUTES.account.delete) + .cookie(cookies) + .to_request(), + ) + .await; + assert_eq!(account_not_found_resp.status(), StatusCode::NOT_FOUND); + let txt: ErrorToResponse = test::read_body_json(account_not_found_resp).await; + assert_eq!(txt.error, format!("{}", ServiceError::AccountNotFound)); +} + +async fn username_update_works(ctx: Arc) { + const NAME: &str = "testuserupda"; + const EMAIL: &str = "testuserupda@sss.com"; + const EMAIL2: &str = "testuserupda2@sss.com"; + const PASSWORD: &str = "longpassword2"; + const NAME2: &str = "terstusrtds"; + const NAME_CHANGE: &str = "terstusrtdsxx"; + + let _ = futures::join!( + ctx.delete_user(NAME, PASSWORD), + ctx.delete_user(NAME2, PASSWORD), + ctx.delete_user(NAME_CHANGE, PASSWORD) + ); + + let _ = ctx.register_and_signin(NAME2, EMAIL2, PASSWORD).await; + let (_creds, signin_resp) = ctx.register_and_signin(NAME, EMAIL, PASSWORD).await; + let cookies = get_cookie!(signin_resp); + let app = get_app!(ctx).await; + + // update username + let mut username_udpate = Username { + username: NAME_CHANGE.into(), + }; + let username_update_resp = test::call_service( + &app, + post_request!(&username_udpate, ROUTES.account.update_username) + .cookie(cookies) + .to_request(), + ) + .await; + assert_eq!(username_update_resp.status(), StatusCode::OK); + + // check duplicate username with duplicate username + username_udpate.username = NAME2.into(); + // ctx.bad_post_req_test( + // NAME_CHANGE, + // PASSWORD, + // ROUTES.account.update_username, + // &username_udpate, + // ServiceError::UsernameTaken, + // ) + // .await; +} + +async fn update_password_works(ctx: Arc) { + const NAME: &str = "updatepassuser"; + const PASSWORD: &str = "longpassword2"; + const EMAIL: &str = "updatepassuser@a.com"; + + let _ = ctx.delete_user(NAME, PASSWORD).await; + + let (_, signin_resp) = ctx.register_and_signin(NAME, EMAIL, PASSWORD).await; + let cookies = get_cookie!(signin_resp); + let app = get_app!(ctx).await; + + let new_password = "newpassword"; + + let update_password = ChangePasswordReqest { + password: PASSWORD.into(), + new_password: new_password.into(), + confirm_new_password: PASSWORD.into(), + }; + + let res = ctx.change_password(NAME, &update_password).await; + assert!(res.is_err()); + assert_eq!(res, Err(ServiceError::PasswordsDontMatch)); + + let update_password = ChangePasswordReqest { + password: PASSWORD.into(), + new_password: new_password.into(), + confirm_new_password: new_password.into(), + }; + + assert!(ctx.change_password(NAME, &update_password).await.is_ok()); + + let update_password = ChangePasswordReqest { + password: new_password.into(), + new_password: new_password.into(), + confirm_new_password: PASSWORD.into(), + }; + + ctx.bad_post_req_test( + NAME, + new_password, + ROUTES.account.update_password, + &update_password, + ServiceError::PasswordsDontMatch, + ) + .await; + + let update_password = ChangePasswordReqest { + password: PASSWORD.into(), + new_password: PASSWORD.into(), + confirm_new_password: PASSWORD.into(), + }; + + ctx.bad_post_req_test( + NAME, + new_password, + ROUTES.account.update_password, + &update_password, + ServiceError::WrongPassword, + ) + .await; + + let update_password = ChangePasswordReqest { + password: new_password.into(), + new_password: PASSWORD.into(), + confirm_new_password: PASSWORD.into(), + }; + + let update_password_resp = test::call_service( + &app, + post_request!(&update_password, ROUTES.account.update_password) + .cookie(cookies) + .to_request(), + ) + .await; + assert_eq!(update_password_resp.status(), StatusCode::OK); +} diff --git a/src/api/v1/auth.rs b/src/api/v1/auth.rs new file mode 100644 index 0000000..d604d08 --- /dev/null +++ b/src/api/v1/auth.rs @@ -0,0 +1,74 @@ +/* + * Copyright (C) 2022 Aravinth Manivannan + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +use crate::ctx::api::v1::auth::{Login, Register}; +use actix_identity::Identity; +use actix_web::http::header; +use actix_web::{web, HttpResponse, Responder}; + +use super::RedirectQuery; +use crate::errors::*; +use crate::AppCtx; + +pub fn services(cfg: &mut web::ServiceConfig) { + cfg.service(register); + cfg.service(login); + cfg.service(signout); +} +#[actix_web_codegen_const_routes::post(path = "crate::V1_API_ROUTES.auth.register")] +#[tracing::instrument(name = "Register new user", skip(ctx, payload))] +async fn register(payload: web::Json, ctx: AppCtx) -> ServiceResult { + ctx.register(&payload).await?; + Ok(HttpResponse::Ok()) +} + +#[actix_web_codegen_const_routes::post(path = "crate::V1_API_ROUTES.auth.login")] +#[tracing::instrument(name = "Login", skip(ctx, payload, id, query))] +async fn login( + id: Identity, + payload: web::Json, + query: web::Query, + ctx: AppCtx, +) -> ServiceResult { + let payload = payload.into_inner(); + let username = ctx.login(&payload).await?; + id.remember(username); + let query = query.into_inner(); + if let Some(redirect_to) = query.redirect_to { + Ok(HttpResponse::Found() + .insert_header((header::LOCATION, redirect_to)) + .finish()) + } else { + Ok(HttpResponse::Ok().into()) + } +} + +#[actix_web_codegen_const_routes::get( + path = "crate::V1_API_ROUTES.auth.logout", + wrap = "super::get_auth_middleware()" +)] +#[tracing::instrument(name = "Sign out", skip(id))] +async fn signout(id: Identity) -> impl Responder { + use actix_auth_middleware::GetLoginRoute; + + if id.identity().is_some() { + id.forget(); + } + HttpResponse::Found() + .append_header((header::LOCATION, crate::V1_API_ROUTES.get_login_route(None))) + .finish() +} diff --git a/src/api/v1/meta.rs b/src/api/v1/meta.rs new file mode 100644 index 0000000..997b3b4 --- /dev/null +++ b/src/api/v1/meta.rs @@ -0,0 +1,114 @@ +/* + * Copyright (C) 2022 Aravinth Manivannan + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +use actix_web::{web, HttpResponse, Responder}; +use serde::{Deserialize, Serialize}; + +use crate::{AppCtx, GIT_COMMIT_HASH, VERSION}; + +#[derive(Clone, Debug, Deserialize, Serialize)] +pub struct BuildDetails<'a> { + pub version: &'a str, + pub git_commit_hash: &'a str, + pub source_code: &'a str, +} + +pub mod routes { + pub struct Meta { + pub build_details: &'static str, + pub health: &'static str, + } + + impl Meta { + pub const fn new() -> Self { + Self { + build_details: "/api/v1/meta/build", + health: "/api/v1/meta/health", + } + } + } +} + +/// emits build details of the binary +#[actix_web_codegen_const_routes::get(path = "crate::V1_API_ROUTES.meta.build_details")] +#[tracing::instrument(name = "Fetch Build Details", skip(ctx))] +async fn build_details(ctx: AppCtx) -> impl Responder { + let build = BuildDetails { + version: VERSION, + git_commit_hash: GIT_COMMIT_HASH, + source_code: &ctx.settings.source_code, + }; + HttpResponse::Ok().json(build) +} + +#[derive(Clone, Debug, Deserialize, Serialize)] +/// Health check return datatype +pub struct Health { + db: bool, +} + +/// checks all components of the system +#[actix_web_codegen_const_routes::get(path = "crate::V1_API_ROUTES.meta.health")] +#[tracing::instrument(name = "Fetch health", skip(ctx))] +async fn health(ctx: crate::AppCtx) -> impl Responder { + let res = Health { + db: ctx.db.ping().await, + }; + + HttpResponse::Ok().json(res) +} + +pub fn services(cfg: &mut web::ServiceConfig) { + cfg.service(build_details); + cfg.service(health); +} + +#[cfg(test)] +mod tests { + use actix_web::{http::StatusCode, test}; + + use crate::*; + + #[actix_rt::test] + async fn build_details_works() { + let (_dir, ctx) = tests::get_ctx().await; + println!("[log] test configuration {:#?}", ctx.settings); + let app = get_app!(ctx).await; + + let resp = get_request!(app, V1_API_ROUTES.meta.build_details); + check_status!(resp, StatusCode::OK); + } + + #[actix_rt::test] + async fn health_works() { + use actix_web::test; + + let (_dir, ctx) = tests::get_ctx().await; + let app = get_app!(ctx).await; + + let resp = test::call_service( + &app, + test::TestRequest::get() + .uri(crate::V1_API_ROUTES.meta.health) + .to_request(), + ) + .await; + assert_eq!(resp.status(), StatusCode::OK); + + let health_resp: super::Health = test::read_body_json(resp).await; + assert!(health_resp.db); + } +} diff --git a/src/api/v1/mod.rs b/src/api/v1/mod.rs new file mode 100644 index 0000000..fae3917 --- /dev/null +++ b/src/api/v1/mod.rs @@ -0,0 +1,44 @@ +/* + * Copyright (C) 2022 Aravinth Manivannan + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +use actix_auth_middleware::Authentication; +use actix_web::web::ServiceConfig; +use serde::Deserialize; + +pub mod account; +pub mod auth; +pub mod meta; +pub mod routes; + +pub use routes::ROUTES; + +pub fn services(cfg: &mut ServiceConfig) { + auth::services(cfg); + account::services(cfg); + meta::services(cfg); +} + +#[derive(Deserialize)] +pub struct RedirectQuery { + pub redirect_to: Option, +} + +pub fn get_auth_middleware() -> Authentication { + Authentication::with_identity(ROUTES) +} + +#[cfg(test)] +mod tests; diff --git a/src/api/v1/pages.rs b/src/api/v1/pages.rs new file mode 100644 index 0000000..0af3686 --- /dev/null +++ b/src/api/v1/pages.rs @@ -0,0 +1,100 @@ +/* + * Copyright (C) 2022 Aravinth Manivannan + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +use actix_web::{web, HttpResponse, Responder}; +use serde::{Deserialize, Serialize}; +use uuid::Uuid; + +use crate::errors::*; +use crate::AppCtx; + +pub mod routes { + pub struct Deploy { + pub update: &'static str, + } + + impl Deploy { + pub const fn new() -> Self { + Self { + update: "/api/v1/update", + } + } + } +} + +#[derive(Clone, Debug, Deserialize, Serialize)] +pub struct DeployEvent { + pub secret: String, + pub branch: String, +} + +#[derive(Clone, Debug, Deserialize, Serialize)] +pub struct DeployEventResp { + pub id: Uuid, +} + +pub fn services(cfg: &mut web::ServiceConfig) { + cfg.service(update); +} + +#[cfg(test)] +mod tests { + use actix_web::{http::StatusCode, test}; + + use crate::tests; + use crate::*; + + use super::*; + + #[actix_rt::test] + async fn deploy_update_works() { + const NAME: &str = "dplyupdwrkuser"; + const PASSWORD: &str = "longpasswordasdfa2"; + const EMAIL: &str = "dplyupdwrkuser@a.com"; + + let (_dir, ctx) = tests::get_ctx().await; + let _ = ctx.delete_user(NAME, PASSWORD).await; + let (_, _signin_resp) = ctx.register_and_signin(NAME, EMAIL, PASSWORD).await; + let page = ctx.add_test_site(NAME.into()).await; + let app = get_app!(ctx).await; + + let mut payload = DeployEvent { + secret: page.secret.clone(), + branch: page.branch.clone(), + }; + + let resp = test::call_service( + &app, + post_request!(&payload, V1_API_ROUTES.deploy.update).to_request(), + ) + .await; + check_status!(resp, StatusCode::OK); + let event_id: DeployEventResp = actix_web::test::read_body_json(resp).await; + let update_event = ctx.db.get_event(&page.domain, &event_id.id).await.unwrap(); + assert_eq!(&update_event.site, &page.domain); + assert_eq!(update_event.id, event_id.id); + + payload.secret = page.branch.clone(); + + let resp = test::call_service( + &app, + post_request!(&payload, V1_API_ROUTES.deploy.update).to_request(), + ) + .await; + check_status!(resp, StatusCode::NOT_FOUND); + } + + } diff --git a/src/api/v1/routes.rs b/src/api/v1/routes.rs new file mode 100644 index 0000000..8d8f8ea --- /dev/null +++ b/src/api/v1/routes.rs @@ -0,0 +1,117 @@ +/* +* Copyright (C) 2022 Aravinth Manivannan +* +* This program is free software: you can redistribute it and/or modify +* it under the terms of the GNU Affero General Public License as +* published by the Free Software Foundation, either version 3 of the +* License, or (at your option) any later version. +* +* This program is distributed in the hope that it will be useful, +* but WITHOUT ANY WARRANTY; without even the implied warranty of +* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +* GNU Affero General Public License for more details. +* +* You should have received a copy of the GNU Affero General Public License +* along with this program. If not, see . +*/ +//! V1 API Routes +use actix_auth_middleware::GetLoginRoute; + +use super::meta::routes::Meta; + +/// constant [Routes](Routes) instance +pub const ROUTES: Routes = Routes::new(); + +/// Authentication routes +pub struct Auth { + /// logout route + pub logout: &'static str, + /// login route + pub login: &'static str, + /// registration route + pub register: &'static str, +} +impl Auth { + /// create new instance of Authentication route + pub const fn new() -> Auth { + let login = "/api/v1/signin"; + let logout = "/api/v1/logout"; + let register = "/api/v1/signup"; + Auth { + logout, + login, + register, + } + } +} + +/// Account management routes +pub struct Account { + /// delete account route + pub delete: &'static str, + /// route to check if an email exists + pub email_exists: &'static str, + /// route to update a user's email + pub update_email: &'static str, + /// route to update password + pub update_password: &'static str, + /// route to check if a username is already registered + pub username_exists: &'static str, + /// route to change username + pub update_username: &'static str, +} + +impl Account { + /// create a new instance of [Account][Account] routes + pub const fn new() -> Account { + let delete = "/api/v1/account/delete"; + let email_exists = "/api/v1/account/email/exists"; + let username_exists = "/api/v1/account/username/exists"; + let update_username = "/api/v1/account/username/update"; + let update_email = "/api/v1/account/email/update"; + let update_password = "/api/v1/account/password/update"; + Account { + delete, + email_exists, + update_email, + update_password, + username_exists, + update_username, + } + } +} + +/// Top-level routes data structure for V1 AP1 +pub struct Routes { + /// Authentication routes + pub auth: Auth, + /// Account routes + pub account: Account, + /// Meta routes + pub meta: Meta, +} + +impl Routes { + /// create new instance of Routes + const fn new() -> Routes { + Routes { + auth: Auth::new(), + account: Account::new(), + meta: Meta::new(), + } + } +} + +impl GetLoginRoute for Routes { + fn get_login_route(&self, src: Option<&str>) -> String { + if let Some(redirect_to) = src { + format!( + "{}?redirect_to={}", + self.auth.login, + urlencoding::encode(redirect_to) + ) + } else { + self.auth.register.to_string() + } + } +} diff --git a/src/api/v1/tests/auth.rs b/src/api/v1/tests/auth.rs new file mode 100644 index 0000000..ab121b5 --- /dev/null +++ b/src/api/v1/tests/auth.rs @@ -0,0 +1,174 @@ +/* + * Copyright (C) 2021 Aravinth Manivannan + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +use actix_auth_middleware::GetLoginRoute; +use actix_web::http::{header, StatusCode}; +use actix_web::test; + +use crate::api::v1::ROUTES; +use crate::ctx::api::v1::auth::{Login, Register}; +use crate::ctx::ArcCtx; +use crate::errors::*; +use crate::*; + +#[actix_rt::test] +async fn postgrest_auth_works() { + let (_, ctx) = crate::tests::get_ctx().await; + auth_works(ctx.clone()).await; + serverside_password_validation_works(ctx).await; +} + +async fn auth_works(ctx: ArcCtx) { + const NAME: &str = "testuserfoo"; + const PASSWORD: &str = "longpassword"; + const EMAIL: &str = "testuser1foo@a.com"; + + let _ = ctx.delete_user(NAME, PASSWORD).await; + let app = get_app!(ctx).await; + + // 1. Register and signin + let (_, signin_resp) = ctx.register_and_signin(NAME, EMAIL, PASSWORD).await; + let cookies = get_cookie!(signin_resp); + + // Sign in with email + ctx.signin_test(EMAIL, PASSWORD).await; + + // 2. check if duplicate username is allowed + let mut msg = Register { + username: NAME.into(), + password: PASSWORD.into(), + confirm_password: PASSWORD.into(), + email: EMAIL.into(), + }; + + msg.username = format!("asdfasd{}", msg.username); + // ctx.bad_post_req_test( + // NAME, + // PASSWORD, + // ROUTES.auth.register, + // &msg, + // ServiceError::EmailTaken, + // ) + // .await; + + msg.email = format!("asdfasd{}", msg.email); + msg.username = NAME.into(); + // ctx.bad_post_req_test( + // NAME, + // PASSWORD, + // ROUTES.auth.register, + // &msg, + // ServiceError::UsernameTaken, + // ) + // .await; + + // 3. sigining in with non-existent user + let mut creds = Login { + login: "nonexistantuser".into(), + password: msg.password.clone(), + }; + ctx.bad_post_req_test( + NAME, + PASSWORD, + ROUTES.auth.login, + &creds, + ServiceError::AccountNotFound, + ) + .await; + + creds.login = "nonexistantuser@example.com".into(); + ctx.bad_post_req_test( + NAME, + PASSWORD, + ROUTES.auth.login, + &creds, + ServiceError::AccountNotFound, + ) + .await; + + // 4. trying to signin with wrong password + creds.login = NAME.into(); + creds.password = NAME.into(); + + ctx.bad_post_req_test( + NAME, + PASSWORD, + ROUTES.auth.login, + &creds, + ServiceError::WrongPassword, + ) + .await; + + // 5. signout + let signout_resp = test::call_service( + &app, + test::TestRequest::get() + .uri(ROUTES.auth.logout) + .cookie(cookies) + .to_request(), + ) + .await; + assert_eq!(signout_resp.status(), StatusCode::FOUND); + let headers = signout_resp.headers(); + assert_eq!( + headers.get(header::LOCATION).unwrap(), + &crate::V1_API_ROUTES.get_login_route(None) + ); + + let creds = Login { + login: NAME.into(), + password: PASSWORD.into(), + }; + + //6. sigin with redirect URL set + let redirect_to = ROUTES.auth.logout; + let resp = test::call_service( + &app, + post_request!(&creds, &ROUTES.get_login_route(Some(redirect_to))).to_request(), + ) + .await; + assert_eq!(resp.status(), StatusCode::FOUND); + let headers = resp.headers(); + assert_eq!(headers.get(header::LOCATION).unwrap(), &redirect_to); +} + +async fn serverside_password_validation_works(ctx: ArcCtx) { + const NAME: &str = "testuser542"; + const PASSWORD: &str = "longpassword2"; + const EMAIL: &str = "testuser542@example.com"; + + let _ = ctx.delete_user(NAME, PASSWORD).await; + + let app = get_app!(ctx).await; + + // checking to see if server-side password validation (password == password_config) + // works + let register_msg = Register { + username: NAME.into(), + password: PASSWORD.into(), + confirm_password: NAME.into(), + email: EMAIL.into(), + }; + let resp = test::call_service( + &app, + post_request!(®ister_msg, ROUTES.auth.register).to_request(), + ) + .await; + assert_eq!(resp.status(), StatusCode::BAD_REQUEST); + let txt: ErrorToResponse = test::read_body_json(resp).await; + assert_eq!(txt.error, format!("{}", ServiceError::PasswordsDontMatch)); +} diff --git a/src/api/v1/tests/mod.rs b/src/api/v1/tests/mod.rs new file mode 100644 index 0000000..09f3aa9 --- /dev/null +++ b/src/api/v1/tests/mod.rs @@ -0,0 +1,18 @@ +/* +* Copyright (C) 2021 Aravinth Manivannan +* +* This program is free software: you can redistribute it and/or modify +* it under the terms of the GNU Affero General Public License as +* published by the Free Software Foundation, either version 3 of the +* License, or (at your option) any later version. +* +* This program is distributed in the hope that it will be useful, +* but WITHOUT ANY WARRANTY; without even the implied warranty of +* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +* GNU Affero General Public License for more details. +* +* You should have received a copy of the GNU Affero General Public License +* along with this program. If not, see . +*/ +mod auth; +mod protected; diff --git a/src/api/v1/tests/protected.rs b/src/api/v1/tests/protected.rs new file mode 100644 index 0000000..f12614c --- /dev/null +++ b/src/api/v1/tests/protected.rs @@ -0,0 +1,70 @@ +/* +* Copyright (C) 2021 Aravinth Manivannan +* +* This program is free software: you can redistribute it and/or modify +* it under the terms of the GNU Affero General Public License as +* published by the Free Software Foundation, either version 3 of the +* License, or (at your option) any later version. +* +* This program is distributed in the hope that it will be useful, +* but WITHOUT ANY WARRANTY; without even the implied warranty of +* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +* GNU Affero General Public License for more details. +* +* You should have received a copy of the GNU Affero General Public License +* along with this program. If not, see . +*/ + +use actix_web::http::StatusCode; +use actix_web::test; + +use crate::ctx::ArcCtx; +//use crate::pages::PAGES; +use crate::*; + +use crate::tests::*; + +#[actix_rt::test] +async fn postgrest_protected_routes_work() { + let (_, ctx) = get_ctx().await; + protected_routes_work(ctx.clone()).await +} + +async fn protected_routes_work(ctx: ArcCtx) { + const NAME: &str = "testuser619"; + const PASSWORD: &str = "longpassword2"; + const EMAIL: &str = "testuser119@a.com2"; + + let _post_protected_urls = [ + "/api/v1/account/secret/", + "/api/v1/account/email/", + "/api/v1/account/delete", + ]; + + let get_protected_urls = [ + V1_API_ROUTES.auth.logout, + // PAGES.auth.logout, + // PAGES.home, + ]; + + let _ = ctx.delete_user(NAME, PASSWORD).await; + + let (_, signin_resp) = ctx.register_and_signin(NAME, EMAIL, PASSWORD).await; + let cookies = get_cookie!(signin_resp); + let app = get_app!(ctx).await; + + for url in get_protected_urls.iter() { + let resp = get_request!(&app, url); + assert_eq!(resp.status(), StatusCode::FOUND); + + let authenticated_resp = get_request!(&app, url, cookies.clone()); + + println!("{url}"); + if url == &V1_API_ROUTES.auth.logout { + // || url == &PAGES.auth.logout { + assert_eq!(authenticated_resp.status(), StatusCode::FOUND); + } else { + assert_eq!(authenticated_resp.status(), StatusCode::OK); + } + } +} diff --git a/src/cache_buster_data.json b/src/cache_buster_data.json new file mode 100644 index 0000000..18ba13c --- /dev/null +++ b/src/cache_buster_data.json @@ -0,0 +1 @@ +{"map":{"./static/cache/css/main.css":"./assets/css/main.C5E0456C4A1FB573F1A5A95D4490E00C60355FBF576EE380B61FB3A5C9D8DBB9.css","./static/cache/css/mobile.css.map":"./assets/css/mobile.css.53B52AE67347949A7066AA3C490CCB02A83D37D6B98A315365C7BB9E21794CA8.map","./static/cache/css/main.css.map":"./assets/css/main.css.F6A1AB66BB2E32F0C4BB1C550A741B010F8FBD134AFF5C65CF9C286D252AE05A.map","./static/cache/css/mobile.css":"./assets/css/mobile.6A41051B957A206B1CD6CFE04FD161558EA3D3EC944CBC191E318F3A765DF75B.css"},"base_dir":"./assets"} \ No newline at end of file diff --git a/src/ctx/api/mod.rs b/src/ctx/api/mod.rs new file mode 100644 index 0000000..5aa8f74 --- /dev/null +++ b/src/ctx/api/mod.rs @@ -0,0 +1,17 @@ +/* + * Copyright (C) 2022 Aravinth Manivannan + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +pub mod v1; diff --git a/src/ctx/api/v1/account.rs b/src/ctx/api/v1/account.rs new file mode 100644 index 0000000..ec16bc6 --- /dev/null +++ b/src/ctx/api/v1/account.rs @@ -0,0 +1,136 @@ +/* + * Copyright (C) 2022 Aravinth Manivannan + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +//! Account management utility datastructures and methods +use serde::{Deserialize, Serialize}; + +pub use super::auth; +use crate::ctx::Ctx; +use crate::db; +use crate::errors::*; + +#[derive(Clone, Debug, Deserialize, Serialize)] +/// Data structure used in `*_exists` methods +pub struct AccountCheckResp { + /// set to true if the attribute in question exists + pub exists: bool, +} + +/// Data structure used to change password of a registered user +#[derive(Clone, Debug, Deserialize, Serialize)] +pub struct ChangePasswordReqest { + /// current password + pub password: String, + /// new password + pub new_password: String, + /// new password confirmation + pub confirm_new_password: String, +} + +impl Ctx { + /// check if email exists on database + pub async fn email_exists(&self, email: &str) -> ServiceResult { + let resp = AccountCheckResp { + exists: self.db.email_exists(email).await?, + }; + + Ok(resp) + } + + /// update email + pub async fn set_email(&self, username: &str, new_email: &str) -> ServiceResult<()> { + self.creds.email(new_email)?; + + let username = self.creds.username(username)?; + + let payload = db::UpdateEmail { + username: &username, + new_email, + }; + self.db.update_email(&payload).await?; + Ok(()) + } + + /// check if email exists in database + pub async fn username_exists(&self, username: &str) -> ServiceResult { + let processed_uname = self.creds.username(username)?; + let resp = AccountCheckResp { + exists: self.db.username_exists(&processed_uname).await?, + }; + Ok(resp) + } + + /// update username of a registered user + pub async fn update_username( + &self, + current_username: &str, + new_username: &str, + ) -> ServiceResult { + let processed_uname = self.creds.username(new_username)?; + + self.db + .update_username(current_username, &processed_uname) + .await?; + + Ok(processed_uname) + } + + // returns Ok(()) upon successful authentication + async fn authenticate(&self, username: &str, password: &str) -> ServiceResult<()> { + use argon2_creds::Config; + let username = self.creds.username(username)?; + let resp = self + .db + .get_password(&db::Login::Username(&username)) + .await?; + if Config::verify(&resp.hash, password)? { + Ok(()) + } else { + Err(ServiceError::WrongPassword) + } + } + + /// delete user + pub async fn delete_user(&self, username: &str, password: &str) -> ServiceResult<()> { + let username = self.creds.username(username)?; + self.authenticate(&username, password).await?; + self.db.delete_user(&username).await?; + Ok(()) + } + + /// change password + pub async fn change_password( + &self, + + username: &str, + payload: &ChangePasswordReqest, + ) -> ServiceResult<()> { + if payload.new_password != payload.confirm_new_password { + return Err(ServiceError::PasswordsDontMatch); + } + + self.authenticate(username, &payload.password).await?; + + let hash = self.creds.password(&payload.new_password)?; + + let username = self.creds.username(username)?; + let db_payload = db::NameHash { username, hash }; + + self.db.update_password(&db_payload).await?; + + Ok(()) + } +} diff --git a/src/ctx/api/v1/auth.rs b/src/ctx/api/v1/auth.rs new file mode 100644 index 0000000..1d3ace3 --- /dev/null +++ b/src/ctx/api/v1/auth.rs @@ -0,0 +1,104 @@ +/* + * Copyright (C) 2022 Aravinth Manivannan + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +//! Authentication helper methods and data structures +use serde::{Deserialize, Serialize}; + +use crate::ctx::Ctx; +use crate::db; +use crate::errors::*; + +/// Register payload +#[derive(Clone, Debug, Deserialize, Serialize)] +pub struct Register { + /// username + pub username: String, + /// password + pub password: String, + /// password confirmation: `password` and `confirm_password` must match + pub confirm_password: String, + pub email: String, +} + +/// Login payload +#[derive(Clone, Debug, Deserialize, Serialize)] +pub struct Login { + // login accepts both username and email under "username field" + // TODO update all instances where login is used + /// user identifier: either username or email + /// an email is detected by checkinf for the existence of `@` character + pub login: String, + /// password + pub password: String, +} + +#[derive(Clone, Debug, Deserialize, Serialize)] +/// struct used to represent password +pub struct Password { + /// password + pub password: String, +} + +impl Ctx { + /// Log in method. Returns `Ok(())` when user is authenticated and errors when authentication + /// fails + pub async fn login(&self, payload: &Login) -> ServiceResult { + use argon2_creds::Config; + + let verify = |stored: &str, received: &str| { + if Config::verify(stored, received)? { + Ok(()) + } else { + Err(ServiceError::WrongPassword) + } + }; + + let creds = if payload.login.contains('@') { + self.db + .get_password(&db::Login::Email(&payload.login)) + .await? + } else { + self.db + .get_password(&db::Login::Username(&payload.login)) + .await? + }; + verify(&creds.hash, &payload.password)?; + Ok(creds.username) + } + + /// register new user + pub async fn register(&self, payload: &Register) -> ServiceResult<()> { + if !self.settings.allow_registration { + return Err(ServiceError::ClosedForRegistration); + } + + if payload.password != payload.confirm_password { + return Err(ServiceError::PasswordsDontMatch); + } + let username = self.creds.username(&payload.username)?; + let hash = self.creds.password(&payload.password)?; + + self.creds.email(&payload.email)?; + + let db_payload = db::Register { + username: &username, + hash: &hash, + email: &payload.email, + }; + + self.db.register(&db_payload).await + } +} diff --git a/src/ctx/api/v1/mod.rs b/src/ctx/api/v1/mod.rs new file mode 100644 index 0000000..41c4b67 --- /dev/null +++ b/src/ctx/api/v1/mod.rs @@ -0,0 +1,22 @@ +/* + * Copyright (C) 2022 Aravinth Manivannan + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +pub mod account; +pub mod auth; +pub mod pages; + +#[cfg(test)] +mod tests; diff --git a/src/ctx/api/v1/pages.rs b/src/ctx/api/v1/pages.rs new file mode 100644 index 0000000..f2a23c2 --- /dev/null +++ b/src/ctx/api/v1/pages.rs @@ -0,0 +1,53 @@ +/* + * Copyright (C) 2022 Aravinth Manivannan + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +use serde::{Deserialize, Serialize}; +use url::Url; + +use crate::ctx::Ctx; +use crate::db; +use crate::db::Site; +use crate::errors::*; +use crate::page::Page; +use crate::settings::Settings; +use crate::utils::get_random; + +#[derive(Clone, Debug, Deserialize, Serialize, Eq, PartialEq)] +/// Data required to add site +pub struct AddSite { + pub url: Url, + pub owner: String, +} + +impl AddSite { + fn to_site(self, s: &Settings) -> Site { + let site_secret = get_random(32); + Site { + url: self.url, + owner: self.owner, + } + } +} + +impl Ctx { + pub async fn add_site(&self, site: AddSite) -> ServiceResult { + let db_site = site.to_site(&self.settings); + self.db.add_site(&db_site).await.unwrap(); + let page = Page::from_site(&self.settings, db_site); + page.archive(&self).await.unwrap(); + Ok(page) + } +} diff --git a/src/ctx/api/v1/tests/accounts.rs b/src/ctx/api/v1/tests/accounts.rs new file mode 100644 index 0000000..79c8904 --- /dev/null +++ b/src/ctx/api/v1/tests/accounts.rs @@ -0,0 +1,227 @@ +/* +* Copyright (C) 2022 Aravinth Manivannan +* +* This program is free software: you can redistribute it and/or modify +* it under the terms of the GNU Affero General Public License as +* published by the Free Software Foundation, either version 3 of the +* License, or (at your option) any later version. +* +* This program is distributed in the hope that it will be useful, +* but WITHOUT ANY WARRANTY; without even the implied warranty of +* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +* GNU Affero General Public License for more details. +* +* You should have received a copy of the GNU Affero General Public License +* along with this program. If not, see . +*/ +use crate::api::v1::account::{Email, Username}; +use crate::ctx::api::v1::account::ChangePasswordReqest; +use crate::ctx::api::v1::auth::Password; +use crate::ctx::api::v1::auth::Register; +use crate::ctx::ArcCtx; +use crate::errors::*; +use crate::*; + +#[actix_rt::test] +async fn postgrest_account_works() { + let (_dir, ctx) = crate::tests::get_ctx().await; + uname_email_exists_works(ctx.clone()).await; + email_udpate_password_validation_del_userworks(ctx.clone()).await; + username_update_works(ctx).await; +} + +async fn uname_email_exists_works(ctx: ArcCtx) { + const NAME: &str = "testuserexistsfoo"; + const NAME2: &str = "testuserexists22"; + const NAME3: &str = "testuserexists32"; + const PASSWORD: &str = "longpassword2"; + const EMAIL: &str = "accotestsuser22@a.com"; + const EMAIL2: &str = "accotestsuser222@a.com"; + const EMAIL3: &str = "accotestsuser322@a.com"; + + let _ = ctx.db.delete_user(NAME).await; + let _ = ctx.db.delete_user(PASSWORD).await; + let _ = ctx.db.delete_user(NAME2).await; + let _ = ctx.db.delete_user(NAME3).await; + + // check username exists for non existent account + println!("{:?}", ctx.username_exists(NAME).await); + assert!(!ctx.username_exists(NAME).await.unwrap().exists); + // check username email for non existent account + assert!(!ctx.email_exists(EMAIL).await.unwrap().exists); + + let mut register_payload = Register { + username: NAME.into(), + password: PASSWORD.into(), + confirm_password: PASSWORD.into(), + email: EMAIL.into(), + }; + ctx.register(®ister_payload).await.unwrap(); + register_payload.username = NAME2.into(); + register_payload.email = EMAIL2.into(); + ctx.register(®ister_payload).await.unwrap(); + + // check username exists + assert!(ctx.username_exists(NAME).await.unwrap().exists); + assert!(ctx.username_exists(NAME2).await.unwrap().exists); + // check email exists + assert!(ctx.email_exists(EMAIL).await.unwrap().exists); + + // update username + ctx.update_username(NAME2, NAME3).await.unwrap(); + assert!(!ctx.username_exists(NAME2).await.unwrap().exists); + assert!(ctx.username_exists(NAME3).await.unwrap().exists); + + // assert!(matches!( + // ctx.update_username(NAME3, NAME).await.err(), + // Some(ServiceError::UsernameTaken) + // )); + + // update email + // assert_eq!( + // ctx.set_email(NAME, EMAIL2).await.err(), + // Some(ServiceError::EmailTaken) + // ); + ctx.set_email(NAME, EMAIL3).await.unwrap(); + + // change password + let mut change_password_req = ChangePasswordReqest { + password: PASSWORD.into(), + new_password: NAME.into(), + confirm_new_password: PASSWORD.into(), + }; + assert_eq!( + ctx.change_password(NAME, &change_password_req).await.err(), + Some(ServiceError::PasswordsDontMatch) + ); + + change_password_req.confirm_new_password = NAME.into(); + ctx.change_password(NAME, &change_password_req) + .await + .unwrap(); +} + +async fn email_udpate_password_validation_del_userworks(ctx: ArcCtx) { + const NAME: &str = "testuser32sd2"; + const PASSWORD: &str = "longpassword2"; + const EMAIL: &str = "testuser12232@a.com2"; + const NAME2: &str = "eupdauser22"; + const EMAIL2: &str = "eupdauser22@a.com"; + + let _ = ctx.delete_user(NAME, PASSWORD).await; + let _ = ctx.delete_user(NAME2, PASSWORD).await; + + let _ = ctx.register_and_signin(NAME2, EMAIL2, PASSWORD).await; + let (_creds, signin_resp) = ctx.register_and_signin(NAME, EMAIL, PASSWORD).await; + let cookies = get_cookie!(signin_resp); + let app = get_app!(ctx).await; + + // update email + let mut email_payload = Email { + email: EMAIL.into(), + }; + let email_update_resp = actix_web::test::call_service( + &app, + post_request!(&email_payload, crate::V1_API_ROUTES.account.update_email) + //post_request!(&email_payload, EMAIL_UPDATE) + .cookie(cookies.clone()) + .to_request(), + ) + .await; + assert_eq!(email_update_resp.status(), StatusCode::OK); + + // check duplicate email while duplicate email + email_payload.email = EMAIL2.into(); + // ctx.bad_post_req_test( + // NAME, + // PASSWORD, + // crate::V1_API_ROUTES.account.update_email, + // &email_payload, + // ServiceError::EmailTaken, + // ) + // .await; + + // wrong password while deleting account + let mut payload = Password { + password: NAME.into(), + }; + ctx.bad_post_req_test( + NAME, + PASSWORD, + V1_API_ROUTES.account.delete, + &payload, + ServiceError::WrongPassword, + ) + .await; + + // delete account + payload.password = PASSWORD.into(); + let delete_user_resp = actix_web::test::call_service( + &app, + post_request!(&payload, crate::V1_API_ROUTES.account.delete) + .cookie(cookies.clone()) + .to_request(), + ) + .await; + + assert_eq!(delete_user_resp.status(), StatusCode::OK); + + // try to delete an account that doesn't exist + let account_not_found_resp = actix_web::test::call_service( + &app, + post_request!(&payload, crate::V1_API_ROUTES.account.delete) + .cookie(cookies) + .to_request(), + ) + .await; + assert_eq!(account_not_found_resp.status(), StatusCode::NOT_FOUND); + let txt: ErrorToResponse = actix_web::test::read_body_json(account_not_found_resp).await; + assert_eq!(txt.error, format!("{}", ServiceError::AccountNotFound)); +} + +async fn username_update_works(ctx: ArcCtx) { + const NAME: &str = "testuse23423rupda"; + const EMAIL: &str = "testu23423serupda@sss.com"; + const EMAIL2: &str = "testu234serupda2@sss.com"; + const PASSWORD: &str = "longpassword2"; + const NAME2: &str = "terstusrt23423ds"; + const NAME_CHANGE: &str = "terstu234234srtdsxx"; + + let _ = futures::join!( + ctx.delete_user(NAME, PASSWORD), + ctx.delete_user(NAME2, PASSWORD), + ctx.delete_user(NAME_CHANGE, PASSWORD) + ); + + let _ = ctx.register_and_signin(NAME2, EMAIL2, PASSWORD).await; + let (_creds, signin_resp) = ctx.register_and_signin(NAME, EMAIL, PASSWORD).await; + let cookies = get_cookie!(signin_resp); + let app = get_app!(ctx).await; + + // update username + let mut username_udpate = Username { + username: NAME_CHANGE.into(), + }; + let username_update_resp = actix_web::test::call_service( + &app, + post_request!( + &username_udpate, + crate::V1_API_ROUTES.account.update_username + ) + .cookie(cookies) + .to_request(), + ) + .await; + assert_eq!(username_update_resp.status(), StatusCode::OK); + + // check duplicate username with duplicate username + username_udpate.username = NAME2.into(); + // ctx.bad_post_req_test( + // NAME_CHANGE, + // PASSWORD, + // V1_API_ROUTES.account.update_username, + // &username_udpate, + // ServiceError::UsernameTaken, + // ) + // .await; +} diff --git a/src/ctx/api/v1/tests/auth.rs b/src/ctx/api/v1/tests/auth.rs new file mode 100644 index 0000000..0181079 --- /dev/null +++ b/src/ctx/api/v1/tests/auth.rs @@ -0,0 +1,104 @@ +/* + * Copyright (C) 2022 Aravinth Manivannan + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +use std::sync::Arc; + +//use crate::api::v1::auth::{Login, Register}; +use crate::ctx::api::v1::auth::{Login, Register}; +use crate::ctx::Ctx; +use crate::errors::*; + +#[actix_rt::test] +async fn postgrest_auth_works() { + let (_dir, ctx) = crate::tests::get_ctx().await; + auth_works(ctx).await; +} + +async fn auth_works(ctx: Arc) { + const NAME: &str = "testuser"; + const PASSWORD: &str = "longpassword"; + const EMAIL: &str = "testuser1@a.com"; + + let _ = ctx.delete_user(NAME, PASSWORD).await; + + // 1. Register with email == None + let mut register_payload = Register { + username: NAME.into(), + password: PASSWORD.into(), + confirm_password: PASSWORD.into(), + email: EMAIL.into(), + }; + + // registration: passwords don't match + register_payload.confirm_password = NAME.into(); + assert!(matches!( + ctx.register(®ister_payload).await.err(), + Some(ServiceError::PasswordsDontMatch) + )); + + register_payload.confirm_password = PASSWORD.into(); + + ctx.register(®ister_payload).await.unwrap(); + // check if duplicate username is allowed + // assert!(matches!( + // ctx.register(®ister_payload).await.err(), + // Some(ServiceError::UsernameTaken) + // )); + + // check if duplicate email is allowed + let name = format!("{}dupemail", NAME); + register_payload.username = name; + // assert!(matches!( + // ctx.register(®ister_payload).await.err(), + // Some(ServiceError::EmailTaken) + // )); + + // Sign in with email + let mut creds = Login { + login: EMAIL.into(), + password: PASSWORD.into(), + }; + ctx.login(&creds).await.unwrap(); + + // signin with username + creds.login = NAME.into(); + ctx.login(&creds).await.unwrap(); + + // sigining in with non-existent username + creds.login = "nonexistantuser".into(); + assert!(matches!( + ctx.login(&creds).await.err(), + Some(ServiceError::AccountNotFound) + )); + + // sigining in with non-existent email + creds.login = "nonexistantuser@example.com".into(); + assert!(matches!( + ctx.login(&creds).await.err(), + Some(ServiceError::AccountNotFound) + )); + + // sign in with incorrect password + creds.login = NAME.into(); + creds.password = NAME.into(); + assert!(matches!( + ctx.login(&creds).await.err(), + Some(ServiceError::WrongPassword) + )); + + // delete user + ctx.delete_user(NAME, PASSWORD).await.unwrap(); +} diff --git a/src/ctx/api/v1/tests/mod.rs b/src/ctx/api/v1/tests/mod.rs new file mode 100644 index 0000000..469eff6 --- /dev/null +++ b/src/ctx/api/v1/tests/mod.rs @@ -0,0 +1,2 @@ +mod accounts; +mod auth; diff --git a/src/ctx/mod.rs b/src/ctx/mod.rs new file mode 100644 index 0000000..596f106 --- /dev/null +++ b/src/ctx/mod.rs @@ -0,0 +1,73 @@ +/* + * Copyright (C) 2022 Aravinth Manivannan + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +use std::sync::Arc; +use std::thread; + +use crate::db::*; +use crate::settings::Settings; +use argon2_creds::{Config as ArgonConfig, ConfigBuilder as ArgonConfigBuilder, PasswordPolicy}; +use reqwest::Client; +use tracing::info; + +pub mod api; + +pub type ArcCtx = Arc; + +#[derive(Clone)] +pub struct Ctx { + pub settings: Settings, + pub db: Database, + /// credential-procession policy + pub creds: ArgonConfig, + pub client: Client, +} + +impl Ctx { + /// Get credential-processing policy + pub fn get_creds() -> ArgonConfig { + ArgonConfigBuilder::default() + .username_case_mapped(true) + .profanity(true) + .blacklist(true) + .password_policy(PasswordPolicy::default()) + .build() + .unwrap() + } + + pub async fn new(settings: Settings) -> Arc { + let creds = Self::get_creds(); + let c = creds.clone(); + + #[allow(unused_variables)] + let init = thread::spawn(move || { + info!("Initializing credential manager"); + c.init(); + info!("Initialized credential manager"); + }); + let db = get_db(&settings).await; + + #[cfg(not(debug_assertions))] + init.join(); + + Arc::new(Self { + settings, + db, + creds, + client: Client::default(), + }) + } +} diff --git a/src/db.rs b/src/db.rs new file mode 100644 index 0000000..94ff8a0 --- /dev/null +++ b/src/db.rs @@ -0,0 +1,624 @@ +/* + * Copyright (C) 2022 Aravinth Manivannan + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +use std::str::FromStr; + +use lazy_static::lazy_static; +use serde::{Deserialize, Serialize}; +use sqlx::sqlite::SqlitePoolOptions; +use sqlx::types::time::OffsetDateTime; +use sqlx::ConnectOptions; +use sqlx::Error; +use sqlx::SqlitePool; +use tracing::error; +use url::Url; +use uuid::Uuid; + +use crate::errors::*; + +/// Connect to databse +pub enum ConnectionOptions { + /// fresh connection + Fresh(Fresh), + /// existing connection + Existing(Conn), +} + +/// Use an existing database pool +pub struct Conn(pub SqlitePool); + +pub struct Fresh { + pub pool_options: SqlitePoolOptions, + pub disable_logging: bool, + pub url: String, +} + +impl ConnectionOptions { + async fn connect(self) -> ServiceResult { + let pool = match self { + Self::Fresh(fresh) => { + println!("from db.rs: {}", fresh.url); + let mut connect_options = + sqlx::sqlite::SqliteConnectOptions::from_str(&fresh.url).unwrap(); + if fresh.disable_logging { + connect_options.disable_statement_logging(); + } + sqlx::sqlite::SqliteConnectOptions::from_str(&fresh.url) + .unwrap() + .disable_statement_logging(); + fresh + .pool_options + .connect_with(connect_options) + .await + .unwrap() + //.map_err(|e| ServiceError::ServiceError(Box::new(e)))? + } + + Self::Existing(conn) => conn.0, + }; + Ok(Database { pool }) + } +} + +#[derive(Clone)] +pub struct Database { + pub pool: SqlitePool, +} + +impl Database { + pub async fn migrate(&self) -> ServiceResult<()> { + sqlx::migrate!("./migrations/") + .run(&self.pool) + .await + .unwrap(); + //.map_err(|e| ServiceError::ServiceError(Box::new(e)))?; + Ok(()) + } + + pub async fn ping(&self) -> bool { + use sqlx::Connection; + + if let Ok(mut con) = self.pool.acquire().await { + con.ping().await.is_ok() + } else { + false + } + } + + /// register a new user + pub async fn register(&self, p: &Register<'_>) -> ServiceResult<()> { + sqlx::query!( + "INSERT INTO librepages_users + (name , password, email) VALUES ($1, $2, $3)", + p.username, + p.hash, + p.email, + ) + .execute(&self.pool) + .await + .map_err(map_register_err)?; + Ok(()) + } + + /// delete a user + pub async fn delete_user(&self, username: &str) -> ServiceResult<()> { + sqlx::query!("DELETE FROM librepages_users WHERE name = ($1)", username) + .execute(&self.pool) + .await + .map_err(|e| map_row_not_found_err(e, ServiceError::AccountNotFound))?; + Ok(()) + } + + /// check if username exists + pub async fn username_exists(&self, username: &str) -> ServiceResult { + let res = sqlx::query!("SELECT ID from librepages_users WHERE name = $1", username,) + .fetch_one(&self.pool) + .await; + + match res { + Ok(_) => Ok(true), + Err(Error::RowNotFound) => Ok(false), + Err(e) => Err(map_register_err(e)), + } + } + + /// get user email + pub async fn get_email(&self, username: &str) -> ServiceResult { + struct Email { + email: String, + } + + let res = sqlx::query_as!( + Email, + "SELECT email FROM librepages_users WHERE name = $1", + username + ) + .fetch_one(&self.pool) + .await + .map_err(|e| map_row_not_found_err(e, ServiceError::AccountNotFound))?; + Ok(res.email) + } + + /// check if email exists + pub async fn email_exists(&self, email: &str) -> ServiceResult { + let res = sqlx::query!("SELECT ID from librepages_users WHERE email = $1", email) + .fetch_one(&self.pool) + .await; + + match res { + Ok(_) => Ok(true), + Err(Error::RowNotFound) => Ok(false), + Err(e) => Err(map_register_err(e)), + } + } + + /// update a user's email + pub async fn update_email(&self, p: &UpdateEmail<'_>) -> ServiceResult<()> { + sqlx::query!( + "UPDATE librepages_users set email = $1 + WHERE name = $2", + p.new_email, + p.username, + ) + .execute(&self.pool) + .await + .map_err(|e| map_row_not_found_err(e, ServiceError::AccountNotFound))?; + + Ok(()) + } + + /// get a user's password + pub async fn get_password(&self, l: &Login<'_>) -> ServiceResult { + struct Password { + name: String, + password: String, + } + + let rec = match l { + Login::Username(u) => sqlx::query_as!( + Password, + r#"SELECT name, password FROM librepages_users WHERE name = ($1)"#, + u, + ) + .fetch_one(&self.pool) + .await + .map_err(|e| map_row_not_found_err(e, ServiceError::AccountNotFound))?, + Login::Email(e) => sqlx::query_as!( + Password, + r#"SELECT name, password FROM librepages_users WHERE email = ($1)"#, + e, + ) + .fetch_one(&self.pool) + .await + .map_err(|e| map_row_not_found_err(e, ServiceError::AccountNotFound))?, + }; + + let res = NameHash { + hash: rec.password, + username: rec.name, + }; + + Ok(res) + } + + /// update user's password + pub async fn update_password(&self, p: &NameHash) -> ServiceResult<()> { + sqlx::query!( + "UPDATE librepages_users set password = $1 + WHERE name = $2", + p.hash, + p.username, + ) + .execute(&self.pool) + .await + .map_err(|e| map_row_not_found_err(e, ServiceError::AccountNotFound))?; + + Ok(()) + } + + /// update username + pub async fn update_username(&self, current: &str, new: &str) -> ServiceResult<()> { + sqlx::query!( + "UPDATE librepages_users set name = $1 + WHERE name = $2", + new, + current, + ) + .execute(&self.pool) + .await + .map_err(|e| map_row_not_found_err(e, ServiceError::AccountNotFound))?; + + Ok(()) + } + + pub async fn add_site(&self, msg: &Site) -> ServiceResult<()> { + let url = msg.url.as_str(); + sqlx::query!( + " + INSERT INTO librepages_sites + (url, owned_by) + VALUES ($1, ( SELECT ID FROM librepages_users WHERE name = $2 )); + ", + url, + msg.owner, + ) + .execute(&self.pool) + .await + .unwrap(); +// .map_err(|e| map_row_not_found_err(e, ServiceError::AccountNotFound))?; + + Ok(()) + } + + pub async fn get_site(&self, owner: &str, url: &str) -> ServiceResult { + let site = sqlx::query_as!( + InnerSite, + "SELECT url + FROM librepages_sites + WHERE owned_by = (SELECT ID FROM librepages_users WHERE name = $1 ) + AND url = $2; + ", + owner, + url + ) + .fetch_one(&self.pool) + .await + .map_err(|e| map_row_not_found_err(e, ServiceError::WebsiteNotFound))?; + + site.to_site(owner.into()) + } + + pub async fn list_all_sites(&self, owner: &str) -> ServiceResult> { + let mut sites = sqlx::query_as!( + InnerSite, + "SELECT url + FROM librepages_sites + WHERE owned_by = (SELECT ID FROM librepages_users WHERE name = $1 ); + ", + owner, + ) + .fetch_all(&self.pool) + .await + .map_err(|e| map_row_not_found_err(e, ServiceError::AccountNotFound))?; + + let mut res = Vec::with_capacity(sites.len()); + + for site in sites.drain(0..) { + res.push(site.to_site(owner.into())?); + } + + Ok(res) + } + + pub async fn delete_site(&self, owner: &str, url: &str) -> ServiceResult<()> { + sqlx::query!( + "DELETE FROM librepages_sites + WHERE url = ($1) + AND owned_by = ( SELECT ID FROM librepages_users WHERE name = $2); + ", + url, + owner + ) + .execute(&self.pool) + .await + .map_err(|e| map_row_not_found_err(e, ServiceError::WebsiteNotFound))?; + Ok(()) + } + + /// check if url exists + pub async fn url_exists(&self, url: &str) -> ServiceResult { + let res = sqlx::query!("SELECT ID from librepages_sites WHERE url = $1", url,) + .fetch_one(&self.pool) + .await; + + match res { + Ok(_) => Ok(true), + Err(Error::RowNotFound) => Ok(false), + Err(e) => Err(map_register_err(e)), + } + } +} +struct InnerSite { + url: String, +} + +impl InnerSite { + fn to_site(self, owner: String) -> ServiceResult { + Ok(Site { + url: Url::parse(&self.url)?, + owner, + }) + } +} + +#[derive(Clone, Debug, Deserialize, Serialize, Eq, PartialEq)] +/// Data required to add a new site +pub struct Site { + pub url: Url, + pub owner: String, +} + +#[derive(Clone, Debug, Deserialize, Serialize, Eq, PartialEq)] +/// Data required to register a new user +pub struct Register<'a> { + /// username of new user + pub username: &'a str, + /// hashed password of new use + pub hash: &'a str, + /// Optionally, email of new use + pub email: &'a str, +} + +#[derive(Clone, Debug, Deserialize, Serialize, Eq, PartialEq)] +/// data required to update them email of a user +pub struct UpdateEmail<'a> { + /// username of the user + pub username: &'a str, + /// new email address of the user + pub new_email: &'a str, +} + +#[derive(Clone, Debug, Deserialize, Serialize, Eq, PartialEq)] +/// types of credentials used as identifiers during login +pub enum Login<'a> { + /// username as login + Username(&'a str), + /// email as login + Email(&'a str), +} + +#[derive(Clone, Debug, Deserialize, Serialize, Eq, PartialEq)] +/// type encapsulating username and hashed password of a user +pub struct NameHash { + /// username + pub username: String, + /// hashed password + pub hash: String, +} +fn now_unix_time_stamp() -> OffsetDateTime { + OffsetDateTime::now_utc() +} + +pub async fn get_db(settings: &crate::settings::Settings) -> Database { + let pool_options = SqlitePoolOptions::new().max_connections(settings.database.pool); + ConnectionOptions::Fresh(Fresh { + pool_options, + url: settings.database.url.clone(), + disable_logging: !settings.debug, + }) + .connect() + .await + .unwrap() +} + +/// map custom row not found error to DB error +pub fn map_row_not_found_err(e: sqlx::Error, row_not_found: ServiceError) -> ServiceError { + if let sqlx::Error::RowNotFound = e { + row_not_found + } else { + map_register_err(e) + } +} + +/// map sqlite errors to [ServiceError](ServiceError) types +fn map_register_err(e: sqlx::Error) -> ServiceError { + use sqlx::Error; + use std::borrow::Cow; + + if let Error::Database(err) = e { + if err.code() == Some(Cow::from("23505")) { + let msg = err.message(); + println!("{}", msg); + if msg.contains("librepages_users_name_key") { + ServiceError::UsernameTaken + } else if msg.contains("librepages_users_email_key") { + ServiceError::EmailTaken + } else { + error!("{}", msg); + ServiceError::InternalServerError + } + } else { + ServiceError::InternalServerError + } + } else { + ServiceError::InternalServerError + } +} + +#[cfg(test)] +mod tests { + use std::collections::HashSet; + + use super::*; + use crate::settings::Settings; + #[actix_rt::test] + async fn db_works() { + let settings = Settings::new().unwrap(); + let pool_options = SqlitePoolOptions::new().max_connections(1); + let db = ConnectionOptions::Fresh(Fresh { + pool_options, + url: settings.database.url.clone(), + disable_logging: !settings.debug, + }) + .connect() + .await + .unwrap(); + assert!(db.ping().await); + + const EMAIL: &str = "sqliteuser@foo.com"; + const EMAIL2: &str = "sqliteuser2@foo.com"; + const NAME: &str = "sqliteuser"; + const PASSWORD: &str = "pasdfasdfasdfadf"; + + db.migrate().await.unwrap(); + let p = super::Register { + username: NAME, + email: EMAIL, + hash: PASSWORD, + }; + + if db.username_exists(p.username).await.unwrap() { + db.delete_user(p.username).await.unwrap(); + assert!( + !db.username_exists(p.username).await.unwrap(), + "user is deleted so username shouldn't exist" + ); + } + + db.register(&p).await.unwrap(); + + // assert!(matches!( + // db.register(&p).await, + // Err(ServiceError::UsernameTaken) + // )); + + // testing get_password + + // with username + let name_hash = db.get_password(&Login::Username(p.username)).await.unwrap(); + assert_eq!(name_hash.hash, p.hash, "user password matches"); + + assert_eq!(name_hash.username, p.username, "username matches"); + + // with email + let mut name_hash = db.get_password(&Login::Email(p.email)).await.unwrap(); + assert_eq!(name_hash.hash, p.hash, "user password matches"); + assert_eq!(name_hash.username, p.username, "username matches"); + + // testing get_email + assert_eq!(db.get_email(p.username).await.unwrap(), p.email); + + // testing email exists + assert!( + db.email_exists(p.email).await.unwrap(), + "user is registered so email should exist" + ); + assert!( + db.username_exists(p.username).await.unwrap(), + "user is registered so username should exist" + ); + + // update password test. setting password = username + name_hash.hash = name_hash.username.clone(); + db.update_password(&name_hash).await.unwrap(); + + let name_hash = db.get_password(&Login::Username(p.username)).await.unwrap(); + assert_eq!( + name_hash.hash, p.username, + "user password matches with changed value" + ); + assert_eq!(name_hash.username, p.username, "username matches"); + + // update username to p.email + assert!( + !db.username_exists(p.email).await.unwrap(), + "user with p.email doesn't exist. pre-check to update username to p.email" + ); + db.update_username(p.username, p.email).await.unwrap(); + assert!( + db.username_exists(p.email).await.unwrap(), + "user with p.email exist post-update" + ); + + // testing update email + let update_email = UpdateEmail { + username: p.username, + new_email: EMAIL2, + }; + db.update_email(&update_email).await.unwrap(); + println!( + "null user email: {}", + db.email_exists(p.email).await.unwrap() + ); + assert!( + db.email_exists(p.email).await.unwrap(), + "user was with empty email but email is set; so email should exist" + ); + + // deleting user + db.delete_user(p.email).await.unwrap(); + assert!( + !db.username_exists(p.email).await.unwrap(), + "user is deleted so username shouldn't exist" + ); + } + + #[actix_rt::test] + pub async fn test_db_sites() { + let settings = Settings::new().unwrap(); + let pool_options = SqlitePoolOptions::new().max_connections(1); + let db = ConnectionOptions::Fresh(Fresh { + pool_options, + url: settings.database.url.clone(), + disable_logging: !settings.debug, + }) + .connect() + .await + .unwrap(); + assert!(db.ping().await); + + const EMAIL: &str = "sqlitedbsiteuser@foo.com"; + const NAME: &str = "sqlitedbsiteuser"; + const PASSWORD: &str = "pasdfasdfasdfadf"; + + db.migrate().await.unwrap(); + + let p = super::Register { + username: NAME, + email: EMAIL, + hash: PASSWORD, + }; + + if db.username_exists(p.username).await.unwrap() { + db.delete_user(p.username).await.unwrap(); + assert!( + !db.username_exists(p.username).await.unwrap(), + "user is deleted so username shouldn't exist" + ); + } + + db.register(&p).await.unwrap(); + + let site = Site { + url: Url::parse("https://db_works.tests.librepages.librepages.org").unwrap(), + owner: p.username.into(), + }; + + // test if url exists. Should be false + assert!(!db.url_exists(site.url.as_str()).await.unwrap()); + + // testing adding site + db.add_site(&site).await.unwrap(); + + // test if url exists. Should be true + assert!(db.url_exists(site.url.as_str()).await.unwrap()); + + // get site + let db_site = db.get_site(p.username, site.url.as_str()).await.unwrap(); + assert_eq!(db_site, site); + + // list all sites owned by user + let db_sites = db.list_all_sites(p.username).await.unwrap(); + assert_eq!(db_sites.len(), 1); + assert_eq!(db_sites, vec![site.clone()]); + + // delete site + db.delete_site(p.username, site.url.as_str()).await.unwrap(); + + // test if url exists. Should be false + assert!(!db.url_exists(site.url.as_str()).await.unwrap()); + } +} diff --git a/src/errors.rs b/src/errors.rs new file mode 100644 index 0000000..08eef20 --- /dev/null +++ b/src/errors.rs @@ -0,0 +1,268 @@ +/* + * Copyright (C) 2022 Aravinth Manivannan + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +//! Represents all the ways a trait can fail using this crate +use std::convert::From; +use std::io::Error as FSErrorInner; +use std::sync::Arc; + +use actix_web::{ + error::ResponseError, + http::{header, StatusCode}, + HttpResponse, HttpResponseBuilder, +}; +use argon2_creds::errors::CredsError; +use config::ConfigError as ConfigErrorInner; +use derive_more::{Display, Error}; +use reqwest::Error as ReqwestError; +use serde::{Deserialize, Serialize}; +use url::ParseError; + +use crate::page::Page; + +#[derive(Debug, Display, Error)] +pub struct FSError(#[display(fmt = "File System Error {}", _0)] pub FSErrorInner); + +#[derive(Debug, Display, Error)] +pub struct ConfigError(#[display(fmt = "Configuration Error {}", _0)] pub ConfigErrorInner); + +#[cfg(not(tarpaulin_include))] +impl PartialEq for FSError { + fn eq(&self, other: &Self) -> bool { + self.0.kind() == other.0.kind() + } +} + +#[cfg(not(tarpaulin_include))] +impl PartialEq for ConfigError { + fn eq(&self, other: &Self) -> bool { + self.0.to_string().trim() == other.0.to_string().trim() + } +} + +#[cfg(not(tarpaulin_include))] +impl From for ServiceError { + fn from(e: FSErrorInner) -> Self { + Self::FSError(FSError(e)) + } +} + +#[cfg(not(tarpaulin_include))] +impl From for ServiceError { + fn from(e: ConfigErrorInner) -> Self { + Self::ConfigError(ConfigError(e)) + } +} + +#[derive(Debug, Display, PartialEq, Error)] +#[cfg(not(tarpaulin_include))] +/// Error data structure grouping various error subtypes +pub enum ServiceError { + /// All non-specific errors are grouped under this category + #[display(fmt = "internal server error")] + InternalServerError, + + #[display(fmt = "The value you entered for URL is not a URL")] //405j + /// The value you entered for url is not url" + NotAUrl, + #[display(fmt = "URL too long, maximum length can't be greater then 2048 characters")] //405 + /// URL too long, maximum length can't be greater then 2048 characters + URLTooLong, + + #[display(fmt = "Website not found")] + /// website not found + WebsiteNotFound, + + #[display(fmt = "File not found")] + /// File not found + FileNotFound, + + /// when the a path configured for a page is already taken + #[display( + fmt = "Path already used for another website. lhs: {:?} rhs: {:?}", + _0, + _1 + )] + PathTaken(Arc, Arc), + + /// when the a Secret configured for a page is already taken + #[display( + fmt = "Secret already used for another website. lhs: {:?} rhs: {:?}", + _0, + _1 + )] + SecretTaken(Arc, Arc), + + /// when the a Repository URL configured for a page is already taken + #[display( + fmt = "Repository URL already configured for another website deployment. lhs: {:?} rhs: {:?}", + _0, + _1 + )] + DuplicateRepositoryURL(Arc, Arc), + + #[display(fmt = "File System Error {}", _0)] + FSError(FSError), + + #[display(fmt = "Unauthorized {}", _0)] + UnauthorizedOperation(#[error(not(source))] String), + + #[display(fmt = "Bad request: {}", _0)] + BadRequest(#[error(not(source))] String), + + #[display(fmt = "Configuration Error {}", _0)] + ConfigError(ConfigError), + #[display(fmt = "Branch {} not found", _0)] + BranchNotFound(#[error(not(source))] String), + + /// Username is taken + #[display(fmt = "Username is taken")] + UsernameTaken, + /// Email is taken + #[display(fmt = "Email is taken")] + EmailTaken, + /// Account not found + #[display(fmt = "Account not found")] + AccountNotFound, + + #[display( + fmt = "This server is is closed for registration. Contact admin if this is unexpecter" + )] + /// registration failure, server is is closed for registration + ClosedForRegistration, + + #[display(fmt = "The value you entered for email is not an email")] //405j + /// The value you entered for email is not an email" + NotAnEmail, + + #[display(fmt = "Wrong password")] + /// wrong password + WrongPassword, + + /// when the value passed contains profanity + #[display(fmt = "Can't allow profanity in usernames")] + ProfanityError, + /// when the value passed contains blacklisted words + /// see [blacklist](https://github.com/shuttlecraft/The-Big-Username-Blacklist) + #[display(fmt = "Username contains blacklisted words")] + BlacklistError, + /// when the value passed contains characters not present + /// in [UsernameCaseMapped](https://tools.ietf.org/html/rfc8265#page-7) + /// profile + #[display(fmt = "username_case_mapped violation")] + UsernameCaseMappedError, + + #[display(fmt = "Passsword too short")] + /// password too short + PasswordTooShort, + #[display(fmt = "password too long")] + /// password too long + PasswordTooLong, + #[display(fmt = "Passwords don't match")] + /// passwords don't match + PasswordsDontMatch, +} + +impl From for ServiceError { + #[cfg(not(tarpaulin_include))] + fn from(e: ReqwestError) -> ServiceError { + tracing::error!("{}", e); + ServiceError::InternalServerError + } +} + +impl From for ServiceError { + #[cfg(not(tarpaulin_include))] + fn from(_: ParseError) -> ServiceError { + ServiceError::NotAUrl + } +} + +/// Generic result data structure +#[cfg(not(tarpaulin_include))] +pub type ServiceResult = std::result::Result; + +#[derive(Serialize, Deserialize, Debug)] +#[cfg(not(tarpaulin_include))] +pub struct ErrorToResponse { + pub error: String, +} + +#[cfg(not(tarpaulin_include))] +impl ResponseError for ServiceError { + #[cfg(not(tarpaulin_include))] + fn error_response(&self) -> HttpResponse { + HttpResponseBuilder::new(self.status_code()) + .append_header((header::CONTENT_TYPE, "application/json; charset=UTF-8")) + .body( + serde_json::to_string(&ErrorToResponse { + error: self.to_string(), + }) + .unwrap(), + ) + } + + #[cfg(not(tarpaulin_include))] + fn status_code(&self) -> StatusCode { + match self { + ServiceError::InternalServerError => StatusCode::INTERNAL_SERVER_ERROR, // INTERNAL SERVER ERROR + ServiceError::ConfigError(_) => StatusCode::INTERNAL_SERVER_ERROR, // INTERNAL SERVER ERROR + ServiceError::NotAUrl => StatusCode::BAD_REQUEST, //BADREQUEST, + ServiceError::URLTooLong => StatusCode::BAD_REQUEST, //BADREQUEST, + ServiceError::WebsiteNotFound => StatusCode::NOT_FOUND, //NOT FOUND, + + ServiceError::PathTaken(_, _) => StatusCode::BAD_REQUEST, //BADREQUEST, + ServiceError::DuplicateRepositoryURL(_, _) => StatusCode::BAD_REQUEST, //BADREQUEST, + ServiceError::SecretTaken(_, _) => StatusCode::BAD_REQUEST, //BADREQUEST, + ServiceError::FSError(_) => StatusCode::INTERNAL_SERVER_ERROR, + + ServiceError::UnauthorizedOperation(_) => StatusCode::UNAUTHORIZED, + ServiceError::BadRequest(_) => StatusCode::BAD_REQUEST, + ServiceError::BranchNotFound(_) => StatusCode::CONFLICT, + + ServiceError::EmailTaken => StatusCode::BAD_REQUEST, + ServiceError::UsernameTaken => StatusCode::BAD_REQUEST, + ServiceError::AccountNotFound => StatusCode::NOT_FOUND, + ServiceError::FileNotFound => StatusCode::NOT_FOUND, + + ServiceError::ProfanityError => StatusCode::BAD_REQUEST, //BADREQUEST, + ServiceError::BlacklistError => StatusCode::BAD_REQUEST, //BADREQUEST, + ServiceError::UsernameCaseMappedError => StatusCode::BAD_REQUEST, //BADREQUEST, + + ServiceError::PasswordTooShort => StatusCode::BAD_REQUEST, //BADREQUEST, + ServiceError::PasswordTooLong => StatusCode::BAD_REQUEST, //BADREQUEST, + ServiceError::PasswordsDontMatch => StatusCode::BAD_REQUEST, //BADREQUEST, + ServiceError::ClosedForRegistration => StatusCode::FORBIDDEN, //FORBIDDEN, + ServiceError::NotAnEmail => StatusCode::BAD_REQUEST, //BADREQUEST, + ServiceError::WrongPassword => StatusCode::UNAUTHORIZED, //UNAUTHORIZED, + } + } +} + +impl From for ServiceError { + #[cfg(not(tarpaulin_include))] + fn from(e: CredsError) -> ServiceError { + match e { + CredsError::UsernameCaseMappedError => ServiceError::UsernameCaseMappedError, + CredsError::ProfainityError => ServiceError::ProfanityError, + CredsError::BlacklistError => ServiceError::BlacklistError, + CredsError::NotAnEmail => ServiceError::NotAnEmail, + CredsError::Argon2Error(_) => ServiceError::InternalServerError, + CredsError::PasswordTooLong => ServiceError::PasswordTooLong, + CredsError::PasswordTooShort => ServiceError::PasswordTooShort, + } + } +} diff --git a/src/main.rs b/src/main.rs new file mode 100644 index 0000000..7c3a55d --- /dev/null +++ b/src/main.rs @@ -0,0 +1,159 @@ +/* + * Copyright (C) 2022 Aravinth Manivannan + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +use std::env; + +use actix_identity::{CookieIdentityPolicy, IdentityService}; +use actix_web::{ + error::InternalError, http::StatusCode, middleware as actix_middleware, web::Data as WebData, + web::JsonConfig, App, HttpServer, +}; +use clap::{Parser, Subcommand}; +use static_assets::FileMap; +use tracing::info; +use tracing_actix_web::TracingLogger; + +mod api; +mod ctx; +mod db; +mod errors; +mod page; +mod pages; +mod serve; +mod settings; +mod static_assets; +#[cfg(test)] +mod tests; +mod utils; + +pub use crate::api::v1::ROUTES as V1_API_ROUTES; +use ctx::Ctx; +pub use settings::Settings; + +pub const CACHE_AGE: u32 = 604800; + +pub const GIT_COMMIT_HASH: &str = env!("GIT_HASH"); +pub const VERSION: &str = env!("CARGO_PKG_VERSION"); +pub const PKG_NAME: &str = env!("CARGO_PKG_NAME"); +pub const PKG_DESCRIPTION: &str = env!("CARGO_PKG_DESCRIPTION"); +pub const PKG_HOMEPAGE: &str = env!("CARGO_PKG_HOMEPAGE"); + +pub type AppCtx = WebData; + +lazy_static::lazy_static! { + pub static ref FILES: FileMap = FileMap::new(); +} + +#[derive(Parser)] +#[clap(author, version, about, long_about = None)] +struct Cli { + #[clap(subcommand)] + command: Commands, +} + +#[derive(Subcommand)] +enum Commands { + /// run database migrations + Migrate, + + /// run server + Serve, +} + +#[actix_web::main] +#[cfg(not(tarpaulin_include))] +async fn main() -> std::io::Result<()> { + if env::var("RUST_LOG").is_err() { + env::set_var("RUST_LOG", "info"); + } + + pretty_env_logger::init(); + let cli = Cli::parse(); + + info!( + "{}: {}.\nFor more information, see: {}\nBuild info:\nVersion: {} commit: {}", + PKG_NAME, PKG_DESCRIPTION, PKG_HOMEPAGE, VERSION, GIT_COMMIT_HASH + ); + + let settings = Settings::new().unwrap(); + + match &cli.command { + Commands::Migrate => db::get_db(&settings).await.migrate().await.unwrap(), + Commands::Serve => { + let ctx = Ctx::new(settings.clone()).await; + let ctx = actix_web::web::Data::new(ctx); + serve(settings, ctx).await.unwrap(); + } + } + Ok(()) +} + +async fn serve(settings: Settings, ctx: AppCtx) -> std::io::Result<()> { + let ip = settings.server.get_ip(); + let workers = settings.server.workers.unwrap_or_else(num_cpus::get); + + info!("Starting server on: http://{}", ip); + HttpServer::new(move || { + App::new() + .wrap(TracingLogger::default()) + .wrap(actix_middleware::Compress::default()) + .app_data(ctx.clone()) + .app_data(get_json_err()) + .wrap(get_identity_service(&(settings.clone()))) + .wrap( + actix_middleware::DefaultHeaders::new() + .add(("Permissions-Policy", "interest-cohort=()")), + ) + .wrap(actix_middleware::NormalizePath::new( + actix_middleware::TrailingSlash::Trim, + )) + .configure(services) + }) + .workers(workers) + .bind(ip) + .unwrap() + .run() + .await +} + +#[cfg(not(tarpaulin_include))] +pub fn get_json_err() -> JsonConfig { + JsonConfig::default().error_handler(|err, _| { + //debug!("JSON deserialization error: {:?}", &err); + InternalError::new(err, StatusCode::BAD_REQUEST).into() + }) +} + +#[cfg(not(tarpaulin_include))] +pub fn get_identity_service(settings: &Settings) -> IdentityService { + let cookie_secret = &settings.server.cookie_secret; + IdentityService::new( + CookieIdentityPolicy::new(cookie_secret.as_bytes()) + .path("/") + .name("Authorization") + //TODO change cookie age + .max_age_secs(216000) + .domain(&settings.server.domain) + .secure(false), + ) +} + +pub fn services(cfg: &mut actix_web::web::ServiceConfig) { + crate::api::v1::services(cfg); + crate::pages::services(cfg); + crate::serve::services(cfg); + crate::static_assets::services(cfg); +} diff --git a/src/page.rs b/src/page.rs new file mode 100644 index 0000000..aa5d297 --- /dev/null +++ b/src/page.rs @@ -0,0 +1,93 @@ +/* + * Copyright (C) 2022 Aravinth Manivannan + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +use std::path::Path; +#[cfg(test)] +use std::println as info; +#[cfg(test)] +use std::println as error; +#[cfg(test)] +use std::println as debug; + +use futures_util::StreamExt; +use reqwest::header::CONTENT_TYPE; +use serde::Deserialize; +use serde::Serialize; +use tokio::fs; +use tokio::io::{self, AsyncWriteExt, BufWriter}; +#[cfg(not(test))] +use tracing::{debug, error, info}; +use url::Url; + +use crate::ctx::Ctx; +use crate::db::Site; +use crate::errors::*; +use crate::settings::Settings; +use crate::utils; + +#[derive(Debug, Clone, Eq, PartialEq, Serialize, Deserialize)] +pub struct Page { + pub file_path: String, + pub url: Url, +} + +impl Page { + pub fn from_site(settings: &Settings, s: Site) -> Self { + Self { + file_path: utils::get_website_path(settings, &s.owner, &s.url) + .to_str() + .unwrap() + .to_owned(), + url: s.url, + } + } + async fn create_parent_dir_all(&self, path: &str) -> ServiceResult<()> { + if let Some(parent) = Path::new(path).parent() { + fs::create_dir_all(parent).await?; + } + Ok(()) + } + + pub async fn archive(&self, ctx: &Ctx) -> ServiceResult<()> { + self.create_parent_dir_all(&self.file_path).await?; + let res = ctx.client.get(self.url.as_str()).send().await?; + let mut fetch_res = false; + if let Some(content_type) = res.headers().get(CONTENT_TYPE) { + if let Ok(content_type) = content_type.to_str() { + if content_type.contains("text/html") { + fetch_res = true; + } + } + } + + let mut bytes = res.bytes_stream(); + let file = fs::OpenOptions::new() + .read(true) + .write(true) + .truncate(true) + .create(true) + .open(&self.file_path) + .await?; + let mut writer = BufWriter::new(file); + + while let Some(item) = bytes.next().await { + let _ = writer.write(&item?).await?; + } + writer.flush().await?; + + Ok(()) + } +} diff --git a/src/pages/auth/login.rs b/src/pages/auth/login.rs new file mode 100644 index 0000000..8b60423 --- /dev/null +++ b/src/pages/auth/login.rs @@ -0,0 +1,121 @@ +/* + * Copyright (C) 2022 Aravinth Manivannan + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +use std::cell::RefCell; + +use actix_identity::Identity; +use actix_web::http::header::ContentType; +use tera::Context; + +use crate::api::v1::RedirectQuery; +use crate::ctx::api::v1::auth::Login as LoginPayload; +use crate::pages::errors::*; +use crate::settings::Settings; +use crate::AppCtx; + +pub use super::*; + +pub struct Login { + ctx: RefCell, +} + +pub const LOGIN: TemplateFile = TemplateFile::new("login", "pages/auth/login.html"); + +impl CtxError for Login { + fn with_error(&self, e: &ReadableError) -> String { + self.ctx.borrow_mut().insert(ERROR_KEY, e); + self.render() + } +} + +impl Login { + pub fn new(settings: &Settings, payload: Option<&LoginPayload>) -> Self { + let ctx = RefCell::new(context(settings)); + if let Some(payload) = payload { + ctx.borrow_mut().insert(PAYLOAD_KEY, payload); + } + Self { ctx } + } + + pub fn render(&self) -> String { + TEMPLATES.render(LOGIN.name, &self.ctx.borrow()).unwrap() + } + + pub fn page(s: &Settings) -> String { + let p = Self::new(s, None); + p.render() + } +} + +#[actix_web_codegen_const_routes::get(path = "PAGES.auth.login")] +#[tracing::instrument(name = "Serve login page", skip(ctx))] +pub async fn get_login(ctx: AppCtx) -> impl Responder { + let login = Login::page(&ctx.settings); + let html = ContentType::html(); + HttpResponse::Ok().content_type(html).body(login) +} + +pub fn services(cfg: &mut web::ServiceConfig) { + cfg.service(get_login); + cfg.service(login_submit); +} + +#[actix_web_codegen_const_routes::post(path = "PAGES.auth.login")] +#[tracing::instrument(name = "Web UI Login", skip(id, payload, query, ctx))] +pub async fn login_submit( + id: Identity, + payload: web::Form, + query: web::Query, + ctx: AppCtx, +) -> PageResult { + let username = ctx + .login(&payload) + .await + .map_err(|e| PageError::new(Login::new(&ctx.settings, Some(&payload)), e))?; + id.remember(username); + let query = query.into_inner(); + if let Some(redirect_to) = query.redirect_to { + Ok(HttpResponse::Found() + .insert_header((http::header::LOCATION, redirect_to)) + .finish()) + } else { + Ok(HttpResponse::Found() + .insert_header((http::header::LOCATION, PAGES.dash.home)) + .finish()) + } +} + +#[cfg(test)] +mod tests { + use super::Login; + use super::LoginPayload; + use crate::errors::*; + use crate::pages::errors::*; + use crate::settings::Settings; + + #[test] + fn register_page_renders() { + let settings = Settings::new().unwrap(); + Login::page(&settings); + let payload = LoginPayload { + login: "foo".into(), + password: "foo".into(), + }; + let page = Login::new(&settings, Some(&payload)); + page.with_error(&ReadableError::new(&ServiceError::WrongPassword)); + page.render(); + } +} diff --git a/src/pages/auth/mod.rs b/src/pages/auth/mod.rs new file mode 100644 index 0000000..5343e89 --- /dev/null +++ b/src/pages/auth/mod.rs @@ -0,0 +1,162 @@ +/* + * Copyright (C) 2022 Aravinth Manivannan + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +use actix_identity::Identity; +use actix_web::*; + +pub use super::{context, Footer, TemplateFile, PAGES, PAYLOAD_KEY, TEMPLATES}; + +pub mod login; +pub mod register; +#[cfg(test)] +mod test; + +pub const AUTH_BASE: TemplateFile = TemplateFile::new("authbase", "pages/auth/base.html"); + +pub fn register_templates(t: &mut tera::Tera) { + for template in [AUTH_BASE, login::LOGIN, register::REGISTER].iter() { + template.register(t).expect(template.name); + } +} + +pub fn services(cfg: &mut web::ServiceConfig) { + cfg.service(signout); + register::services(cfg); + login::services(cfg); +} + +#[actix_web_codegen_const_routes::get( + path = "PAGES.auth.logout", + wrap = "super::get_auth_middleware()" +)] +#[tracing::instrument(name = "Sign out", skip(id))] +async fn signout(id: Identity) -> impl Responder { + use actix_auth_middleware::GetLoginRoute; + + if id.identity().is_some() { + id.forget(); + } + HttpResponse::Found() + .append_header((http::header::LOCATION, PAGES.get_login_route(None))) + .finish() +} + +//#[post(path = "PAGES.auth.login")] +//pub async fn login_submit( +// id: Identity, +// payload: web::Form, +// data: AppData, +//) -> PageResult { +// let payload = payload.into_inner(); +// match runners::login_runner(&payload, &data).await { +// Ok(username) => { +// id.remember(username); +// Ok(HttpResponse::Found() +// .insert_header((header::LOCATION, PAGES.home)) +// .finish()) +// } +// Err(e) => { +// let status = e.status_code(); +// let heading = status.canonical_reason().unwrap_or("Error"); +// +// Ok(HttpResponseBuilder::new(status) +// .content_type("text/html; charset=utf-8") +// .body( +// IndexPage::new(heading, &format!("{}", e)) +// .render_once() +// .unwrap(), +// )) +// } +// } +//} +// +//#[cfg(test)] +//mod tests { +// use actix_web::test; +// +// use super::*; +// +// use crate::api::v1::auth::runners::{Login, Register}; +// use crate::data::Data; +// use crate::tests::*; +// use crate::*; +// use actix_web::http::StatusCode; +// +// #[actix_rt::test] +// async fn auth_form_works() { +// let data = Data::new().await; +// const NAME: &str = "testuserform"; +// const PASSWORD: &str = "longpassword"; +// +// let app = get_app!(data).await; +// +// delete_user(NAME, &data).await; +// +// // 1. Register with email == None +// let msg = Register { +// username: NAME.into(), +// password: PASSWORD.into(), +// confirm_password: PASSWORD.into(), +// email: None, +// }; +// let resp = test::call_service( +// &app, +// post_request!(&msg, V1_API_ROUTES.auth.register).to_request(), +// ) +// .await; +// assert_eq!(resp.status(), StatusCode::OK); +// +// // correct form login +// let msg = Login { +// login: NAME.into(), +// password: PASSWORD.into(), +// }; +// +// let resp = test::call_service( +// &app, +// post_request!(&msg, PAGES.auth.login, FORM).to_request(), +// ) +// .await; +// assert_eq!(resp.status(), StatusCode::FOUND); +// let headers = resp.headers(); +// assert_eq!(headers.get(header::LOCATION).unwrap(), PAGES.home,); +// +// // incorrect form login +// let msg = Login { +// login: NAME.into(), +// password: NAME.into(), +// }; +// let resp = test::call_service( +// &app, +// post_request!(&msg, PAGES.auth.login, FORM).to_request(), +// ) +// .await; +// assert_eq!(resp.status(), StatusCode::UNAUTHORIZED); +// +// // non-existent form login +// let msg = Login { +// login: PASSWORD.into(), +// password: PASSWORD.into(), +// }; +// let resp = test::call_service( +// &app, +// post_request!(&msg, PAGES.auth.login, FORM).to_request(), +// ) +// .await; +// assert_eq!(resp.status(), StatusCode::NOT_FOUND); +// } +//} +// diff --git a/src/pages/auth/register.rs b/src/pages/auth/register.rs new file mode 100644 index 0000000..32330a6 --- /dev/null +++ b/src/pages/auth/register.rs @@ -0,0 +1,109 @@ +/* + * Copyright (C) 2022 Aravinth Manivannan + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +use actix_web::http::header::ContentType; +use std::cell::RefCell; +use tera::Context; + +use crate::ctx::api::v1::auth::Register as RegisterPayload; +use crate::pages::errors::*; +use crate::settings::Settings; +use crate::AppCtx; + +pub use super::*; + +pub const REGISTER: TemplateFile = TemplateFile::new("register", "pages/auth/register.html"); + +pub struct Register { + ctx: RefCell, +} + +impl CtxError for Register { + fn with_error(&self, e: &ReadableError) -> String { + self.ctx.borrow_mut().insert(ERROR_KEY, e); + self.render() + } +} + +impl Register { + fn new(settings: &Settings, payload: Option<&RegisterPayload>) -> Self { + let ctx = RefCell::new(context(settings)); + if let Some(payload) = payload { + ctx.borrow_mut().insert(PAYLOAD_KEY, payload); + } + Self { ctx } + } + + pub fn render(&self) -> String { + TEMPLATES.render(REGISTER.name, &self.ctx.borrow()).unwrap() + } + + pub fn page(s: &Settings) -> String { + let p = Self::new(s, None); + p.render() + } +} + +#[actix_web_codegen_const_routes::get(path = "PAGES.auth.register")] +#[tracing::instrument(name = "Serve registration page", skip(ctx))] +pub async fn get_register(ctx: AppCtx) -> impl Responder { + let login = Register::page(&ctx.settings); + let html = ContentType::html(); + HttpResponse::Ok().content_type(html).body(login) +} + +pub fn services(cfg: &mut web::ServiceConfig) { + cfg.service(get_register); + cfg.service(register_submit); +} + +#[actix_web_codegen_const_routes::post(path = "PAGES.auth.register")] +#[tracing::instrument(name = "Process web UI registration", skip(ctx))] +pub async fn register_submit( + payload: web::Form, + ctx: AppCtx, +) -> PageResult { + ctx.register(&payload) + .await + .map_err(|e| PageError::new(Register::new(&ctx.settings, Some(&payload)), e))?; + Ok(HttpResponse::Found() + .insert_header((http::header::LOCATION, PAGES.auth.login)) + .finish()) +} + +#[cfg(test)] +mod tests { + use super::Register; + use super::RegisterPayload; + use crate::errors::*; + use crate::pages::errors::*; + use crate::settings::Settings; + + #[test] + fn register_page_renders() { + let settings = Settings::new().unwrap(); + Register::page(&settings); + let payload = RegisterPayload { + username: "foo".into(), + password: "foo".into(), + confirm_password: "foo".into(), + email: "foo".into(), + }; + let page = Register::new(&settings, Some(&payload)); + page.with_error(&ReadableError::new(&ServiceError::WrongPassword)); + page.render(); + } +} diff --git a/src/pages/auth/test.rs b/src/pages/auth/test.rs new file mode 100644 index 0000000..ca1796f --- /dev/null +++ b/src/pages/auth/test.rs @@ -0,0 +1,144 @@ +/* + * Copyright (C) 2022 Aravinth Manivannan + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +use actix_auth_middleware::GetLoginRoute; + +use actix_web::http::header; +use actix_web::http::StatusCode; +use actix_web::test; + +use super::*; + +use crate::ctx::api::v1::auth::{Login, Register}; +use crate::ctx::ArcCtx; +use crate::errors::*; +use crate::tests::*; +use crate::*; + +#[actix_rt::test] +async fn postgrest_pages_auth_works() { + let (_, ctx) = get_ctx().await; + auth_works(ctx.clone()).await; + serverside_password_validation_works(ctx.clone()).await; +} + +async fn auth_works(ctx: ArcCtx) { + const NAME: &str = "testuserform"; + const EMAIL: &str = "testuserform@foo.com"; + const PASSWORD: &str = "longpassword"; + + let _ = ctx.delete_user(NAME, PASSWORD).await; + let app = get_app!(ctx).await; + + // 1. Register with email + let msg = Register { + username: NAME.into(), + password: PASSWORD.into(), + confirm_password: PASSWORD.into(), + email: EMAIL.into(), + }; + let resp = test::call_service( + &app, + post_request!(&msg, PAGES.auth.register, FORM).to_request(), + ) + .await; + assert_eq!(resp.status(), StatusCode::FOUND); + let headers = resp.headers(); + assert_eq!(headers.get(header::LOCATION).unwrap(), PAGES.auth.login); + + // sign in + let msg = Login { + login: NAME.into(), + password: PASSWORD.into(), + }; + let resp = test::call_service( + &app, + post_request!(&msg, PAGES.auth.login, FORM).to_request(), + ) + .await; + assert_eq!(resp.status(), StatusCode::FOUND); + let headers = resp.headers(); + assert_eq!(headers.get(header::LOCATION).unwrap(), PAGES.dash.home); + let cookies = get_cookie!(resp); + + // redirect after signin + let redirect = "/foo/bar/nonexistantuser"; + let url = PAGES.get_login_route(Some(redirect)); + let resp = test::call_service(&app, post_request!(&msg, &url, FORM).to_request()).await; + assert_eq!(resp.status(), StatusCode::FOUND); + let headers = resp.headers(); + assert_eq!(headers.get(header::LOCATION).unwrap(), &redirect); + + // wrong password signin + let msg = Login { + login: NAME.into(), + password: NAME.into(), + }; + let resp = test::call_service( + &app, + post_request!(&msg, PAGES.auth.login, FORM).to_request(), + ) + .await; + assert_eq!(resp.status(), ServiceError::WrongPassword.status_code()); + + // signout + + println!("{}", PAGES.auth.logout); + let signout_resp = test::call_service( + &app, + test::TestRequest::get() + .uri(PAGES.auth.logout) + .cookie(cookies) + .to_request(), + ) + .await; + assert_eq!(signout_resp.status(), StatusCode::FOUND); + let headers = signout_resp.headers(); + assert_eq!( + headers.get(header::LOCATION).unwrap(), + &PAGES.get_login_route(None) + ); + + let _ = ctx.delete_user(NAME, PASSWORD).await; +} + +async fn serverside_password_validation_works(ctx: ArcCtx) { + const NAME: &str = "pagetestuser542"; + const EMAIL: &str = "pagetestuser542@foo.com"; + const PASSWORD: &str = "longpassword2"; + + let _ = ctx.delete_user(NAME, PASSWORD).await; + + let app = get_app!(ctx).await; + + // checking to see if server-side password validation (password == password_config) + // works + let register_msg = Register { + username: NAME.into(), + password: PASSWORD.into(), + confirm_password: NAME.into(), + email: EMAIL.into(), + }; + let resp = test::call_service( + &app, + post_request!(®ister_msg, PAGES.auth.register, FORM).to_request(), + ) + .await; + assert_eq!( + resp.status(), + ServiceError::PasswordsDontMatch.status_code() + ); +} diff --git a/src/pages/dash/home.rs b/src/pages/dash/home.rs new file mode 100644 index 0000000..ddf339f --- /dev/null +++ b/src/pages/dash/home.rs @@ -0,0 +1,76 @@ +/* + * Copyright (C) 2022 Aravinth Manivannan + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +use std::cell::RefCell; + +use actix_identity::Identity; +use actix_web::http::header::ContentType; +use serde::{Deserialize, Serialize}; +use tera::Context; + +use super::get_auth_middleware; +use crate::db::Site; +use crate::errors::ServiceResult; +use crate::pages::errors::*; +use crate::settings::Settings; +use crate::AppCtx; + +pub use super::*; + +pub const DASH_HOME: TemplateFile = TemplateFile::new("dash_home", "pages/dash/index.html"); + +pub struct Home { + ctx: RefCell, +} + +impl CtxError for Home { + fn with_error(&self, e: &ReadableError) -> String { + self.ctx.borrow_mut().insert(ERROR_KEY, e); + self.render() + } +} + +impl Home { + pub fn new(settings: &Settings, sites: Option<&[Site]>) -> Self { + let ctx = RefCell::new(context(settings)); + if let Some(sites) = sites { + ctx.borrow_mut().insert(PAYLOAD_KEY, sites); + } + Self { ctx } + } + + pub fn render(&self) -> String { + TEMPLATES + .render(DASH_HOME.name, &self.ctx.borrow()) + .unwrap() + } +} + + +#[actix_web_codegen_const_routes::get(path = "PAGES.dash.home", wrap = "get_auth_middleware()")] +#[tracing::instrument(name = "Dashboard homepage", skip(ctx, id))] +pub async fn get_home(ctx: AppCtx, id: Identity) -> PageResult { + let db_sites = ctx.db.list_all_sites(&id.identity().unwrap()) + .await + .map_err(|e| PageError::new(Home::new(&ctx.settings, None), e))?; + let home = Home::new(&ctx.settings, Some(&db_sites)).render(); + let html = ContentType::html(); + Ok(HttpResponse::Ok().content_type(html).body(home)) +} + +pub fn services(cfg: &mut web::ServiceConfig) { + cfg.service(get_home); +} diff --git a/src/pages/dash/mod.rs b/src/pages/dash/mod.rs new file mode 100644 index 0000000..9e897df --- /dev/null +++ b/src/pages/dash/mod.rs @@ -0,0 +1,35 @@ +/* + * Copyright (C) 2022 Aravinth Manivannan + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +use actix_web::*; +use serde::{Deserialize, Serialize}; +use uuid::Uuid; + +use super::get_auth_middleware; +pub use super::{context, Footer, TemplateFile, PAGES, PAYLOAD_KEY, TEMPLATES}; + +pub mod home; +pub mod sites; + +pub fn register_templates(t: &mut tera::Tera) { + home::DASH_HOME.register(t).expect(home::DASH_HOME.name); + sites::ADD_SITE.register(t).expect(sites::ADD_SITE.name); +} + +pub fn services(cfg: &mut web::ServiceConfig) { + home::services(cfg); + sites::services(cfg); +} diff --git a/src/pages/dash/sites.rs b/src/pages/dash/sites.rs new file mode 100644 index 0000000..c2f09b1 --- /dev/null +++ b/src/pages/dash/sites.rs @@ -0,0 +1,100 @@ +/* + * Copyright (C) 2022 Aravinth Manivannan + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +use std::cell::RefCell; + +use actix_identity::Identity; +use actix_web::http::header::ContentType; +use serde::{Deserialize, Serialize}; +use tera::Context; +use url::Url; + +use super::get_auth_middleware; +use crate::ctx::api::v1::pages; +use crate::errors::ServiceResult; +use crate::pages::errors::*; +use crate::settings::Settings; +use crate::AppCtx; + +pub use super::*; + +pub const ADD_SITE: TemplateFile = TemplateFile::new("dash_add_site", "pages/dash/sites/add.html"); + +pub struct AddSite { + ctx: RefCell, +} + +impl CtxError for AddSite { + fn with_error(&self, e: &ReadableError) -> String { + self.ctx.borrow_mut().insert(ERROR_KEY, e); + self.render() + } +} + +impl AddSite { + pub fn new(settings: &Settings) -> Self { + let ctx = RefCell::new(context(settings)); + Self { ctx } + } + + pub fn render(&self) -> String { + TEMPLATES.render(ADD_SITE.name, &self.ctx.borrow()).unwrap() + } +} + +#[actix_web_codegen_const_routes::get(path = "PAGES.dash.add_site", wrap = "get_auth_middleware()")] +#[tracing::instrument(name = "get add site", skip(ctx, id))] +pub async fn get_add_site(ctx: AppCtx, id: Identity) -> PageResult { + let home = AddSite::new(&ctx.settings).render(); + let html = ContentType::html(); + Ok(HttpResponse::Ok().content_type(html).body(home)) +} + +#[derive(Clone, Debug, Deserialize, Serialize, Eq, PartialEq)] +/// Data required to add site +pub struct TemplatePayloadAddSite { + pub url: Url, +} + +#[actix_web_codegen_const_routes::post( + path = "PAGES.dash.add_site", + wrap = "get_auth_middleware()" +)] +#[tracing::instrument(name = "Post add site", skip(ctx, id))] +pub async fn post_add_site( + ctx: AppCtx, + id: Identity, + payload: web::Form, +) -> PageResult { + let payload = payload.into_inner(); + let owner = id.identity().unwrap(); + let location = format!("{}?url={}", PAGES.serve.catch_all, &payload.url); + ctx.add_site(pages::AddSite { + url: payload.url, + owner, + }) + .await + .unwrap(); + Ok(HttpResponse::Found() + .append_header((http::header::LOCATION, location)) + .finish()) + +} + +pub fn services(cfg: &mut web::ServiceConfig) { + cfg.service(get_add_site); + cfg.service(post_add_site); +} diff --git a/src/pages/errors.rs b/src/pages/errors.rs new file mode 100644 index 0000000..16c4862 --- /dev/null +++ b/src/pages/errors.rs @@ -0,0 +1,105 @@ +/* + * Copyright (C) 2022 Aravinth Manivannan + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +use std::fmt; + +use actix_web::{ + error::ResponseError, + http::{header::ContentType, StatusCode}, + HttpResponse, HttpResponseBuilder, +}; +use derive_more::Display; +use derive_more::Error; +use serde::*; + +use super::TemplateFile; +use crate::errors::ServiceError; + +pub const ERROR_KEY: &str = "error"; + +pub const ERROR_TEMPLATE: TemplateFile = TemplateFile::new("error_comp", "components/error.html"); +pub fn register_templates(t: &mut tera::Tera) { + ERROR_TEMPLATE.register(t).expect(ERROR_TEMPLATE.name); +} + +/// Render template with error context +pub trait CtxError { + fn with_error(&self, e: &ReadableError) -> String; +} + +#[derive(Serialize, Debug, Display, Clone)] +#[display(fmt = "title: {} reason: {}", title, reason)] +pub struct ReadableError { + pub reason: String, + pub title: String, +} + +impl ReadableError { + pub fn new(e: &ServiceError) -> Self { + let reason = format!("{}", e); + let title = format!("{}", e.status_code()); + + Self { reason, title } + } +} + +#[derive(Error, Display)] +#[display(fmt = "{}", readable)] +pub struct PageError { + #[error(not(source))] + template: T, + readable: ReadableError, + #[error(not(source))] + error: ServiceError, +} + +impl fmt::Debug for PageError { + #[cfg(not(tarpaulin_include))] + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.debug_struct("PageError") + .field("readable", &self.readable) + .finish() + } +} + +impl PageError { + /// create new instance of [PageError] from a template and an error + pub fn new(template: T, error: ServiceError) -> Self { + let readable = ReadableError::new(&error); + Self { + error, + template, + readable, + } + } +} + +#[cfg(not(tarpaulin_include))] +impl ResponseError for PageError { + fn error_response(&self) -> HttpResponse { + HttpResponseBuilder::new(self.status_code()) + .content_type(ContentType::html()) + .body(self.template.with_error(&self.readable)) + } + + fn status_code(&self) -> StatusCode { + self.error.status_code() + } +} + +/// Generic result data structure +#[cfg(not(tarpaulin_include))] +pub type PageResult = std::result::Result>; diff --git a/src/pages/mod.rs b/src/pages/mod.rs new file mode 100644 index 0000000..c75f655 --- /dev/null +++ b/src/pages/mod.rs @@ -0,0 +1,209 @@ +/* + * Copyright (C) 2022 Aravinth Manivannan + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +use std::cell::RefCell; + +use actix_identity::Identity; +use actix_web::http::header; +use actix_web::*; +use lazy_static::lazy_static; +use rust_embed::RustEmbed; +use serde::*; +use tera::*; + +use crate::pages::errors::*; +use crate::settings::Settings; +use crate::static_assets::ASSETS; +use crate::AppCtx; +use crate::{GIT_COMMIT_HASH, VERSION}; + +pub mod auth; +pub mod dash; +pub mod errors; +pub mod routes; + +pub use routes::get_auth_middleware; +pub use routes::PAGES; + +pub struct TemplateFile { + pub name: &'static str, + pub path: &'static str, +} + +impl TemplateFile { + pub const fn new(name: &'static str, path: &'static str) -> Self { + Self { name, path } + } + + pub fn register(&self, t: &mut Tera) -> std::result::Result<(), tera::Error> { + t.add_raw_template(self.name, &Templates::get_template(self).expect(self.name)) + } + + #[cfg(test)] + #[allow(dead_code)] + pub fn register_from_file(&self, t: &mut Tera) -> std::result::Result<(), tera::Error> { + use std::path::Path; + t.add_template_file(Path::new("templates/").join(self.path), Some(self.name)) + } +} + +pub const PAYLOAD_KEY: &str = "payload"; + +pub const BASE: TemplateFile = TemplateFile::new("base", "components/base.html"); +pub const FOOTER: TemplateFile = TemplateFile::new("footer", "components/footer.html"); +pub const PUB_NAV: TemplateFile = TemplateFile::new("pub_nav", "components/nav/pub.html"); +pub const AUTH_NAV: TemplateFile = TemplateFile::new("auth_nav", "components/nav/auth.html"); + +lazy_static! { + pub static ref TEMPLATES: Tera = { + let mut tera = Tera::default(); + for t in [BASE, FOOTER, PUB_NAV, AUTH_NAV].iter() { + t.register(&mut tera).unwrap(); + } + errors::register_templates(&mut tera); + tera.autoescape_on(vec![".html", ".sql"]); + auth::register_templates(&mut tera); + dash::register_templates(&mut tera); + tera + }; +} + +#[derive(RustEmbed)] +#[folder = "templates/"] +pub struct Templates; + +impl Templates { + pub fn get_template(t: &TemplateFile) -> Option { + match Self::get(t.path) { + Some(file) => Some(String::from_utf8_lossy(&file.data).into_owned()), + None => None, + } + } +} + +pub fn context(s: &Settings) -> Context { + let mut ctx = Context::new(); + let footer = Footer::new(s); + ctx.insert("footer", &footer); + ctx.insert("page", &PAGES); + ctx.insert("assets", &*ASSETS); + ctx +} + +pub fn auth_ctx(_username: Option<&str>, s: &Settings) -> Context { + let mut ctx = Context::new(); + let footer = Footer::new(s); + ctx.insert("footer", &footer); + ctx.insert("page", &PAGES); + ctx.insert("assets", &*ASSETS); + // ctx.insert("loggedin_user", &profile_link); + ctx +} + +#[derive(Serialize)] +pub struct Footer<'a> { + version: &'a str, + support_email: &'a str, + source_code: &'a str, + git_hash: &'a str, + settings: &'a Settings, +} + +impl<'a> Footer<'a> { + pub fn new(settings: &'a Settings) -> Self { + Self { + version: VERSION, + source_code: &settings.source_code, + support_email: &settings.support_email, + git_hash: &GIT_COMMIT_HASH[..8], + settings, + } + } +} + +#[actix_web_codegen_const_routes::get(path = "PAGES.home")] +#[tracing::instrument(name = "Serve index page", skip(ctx, id))] +pub async fn home(ctx: AppCtx, id: Identity) -> HttpResponse { + let location = if id.identity().is_some() { + PAGES.auth.login + } else { + PAGES.dash.home + }; + HttpResponse::Found() + .append_header((header::LOCATION, location)) + .finish() +} + +pub fn services(cfg: &mut web::ServiceConfig) { + dash::services(cfg); + auth::services(cfg); + cfg.service(home); +} + +#[cfg(test)] +mod tests { + + #[test] + fn templates_work_basic() { + use super::*; + use tera::Tera; + + let mut tera = Tera::default(); + let mut tera2 = Tera::default(); + for t in [ + BASE, + FOOTER, + PUB_NAV, + AUTH_NAV, + auth::AUTH_BASE, + auth::login::LOGIN, + auth::register::REGISTER, + errors::ERROR_TEMPLATE, + super::dash::home::DASH_HOME, + ] + .iter() + { + t.register_from_file(&mut tera2).unwrap(); + t.register(&mut tera).unwrap(); + } + } +} + +#[cfg(test)] +mod http_page_tests { + use actix_web::http::StatusCode; + use actix_web::test; + + use crate::ctx::ArcCtx; + use crate::*; + + use super::PAGES; + + #[actix_rt::test] + async fn postgrest_templates_work() { + let (_, ctx) = crate::tests::get_ctx().await; + templates_work(ctx).await; + } + + async fn templates_work(ctx: ArcCtx) { + let app = get_app!(ctx).await; + + for file in [PAGES.auth.login, PAGES.auth.register, PAGES.home].iter() { + let resp = get_request!(&app, file); + assert_eq!(resp.status(), StatusCode::OK); + } + } +} diff --git a/src/pages/routes.rs b/src/pages/routes.rs new file mode 100644 index 0000000..3013007 --- /dev/null +++ b/src/pages/routes.rs @@ -0,0 +1,105 @@ +/* + * Copyright (C) 2022 Aravinth Manivannan + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +use actix_auth_middleware::{Authentication, GetLoginRoute}; +use serde::*; + +use crate::serve::routes::Serve; + +/// constant [Pages](Pages) instance +pub const PAGES: Pages = Pages::new(); + +#[derive(Serialize)] +/// Top-level routes data structure for V1 AP1 +pub struct Pages { + /// Authentication routes + pub auth: Auth, + /// home page + pub home: &'static str, + pub dash: Dash, + pub serve: Serve, +} + +impl Pages { + /// create new instance of Routes + const fn new() -> Pages { + let auth = Auth::new(); + let dash = Dash::new(); + let serve = Serve::new(); + let home = "/"; + Pages { auth, home, dash, serve } + } +} + +#[derive(Serialize)] +/// Authentication routes +pub struct Auth { + /// logout route + pub logout: &'static str, + /// login route + pub login: &'static str, + /// registration route + pub register: &'static str, +} + +impl Auth { + /// create new instance of Authentication route + pub const fn new() -> Auth { + let login = "/login"; + let logout = "/logout"; + let register = "/join"; + Auth { + logout, + login, + register, + } + } +} + +#[derive(Serialize)] +/// Dashboard routes +pub struct Dash { + /// home route + pub home: &'static str, + pub add_site: &'static str, +} + +impl Dash { + /// create new instance of Dash route + pub const fn new() -> Dash { + let home = "/dash"; + let add_site = "/dash/sites/add"; + Dash { home, add_site } + } +} + +pub fn get_auth_middleware() -> Authentication { + Authentication::with_identity(PAGES) +} + +impl GetLoginRoute for Pages { + fn get_login_route(&self, src: Option<&str>) -> String { + if let Some(redirect_to) = src { + format!( + "{}?redirect_to={}", + self.auth.login, + urlencoding::encode(redirect_to) + ) + } else { + self.auth.login.to_string() + } + } +} diff --git a/src/serve.rs b/src/serve.rs new file mode 100644 index 0000000..2f7a0cb --- /dev/null +++ b/src/serve.rs @@ -0,0 +1,76 @@ +use std::convert::identity; + +use actix_identity::Identity; +/* + * Copyright (C) 2022 Aravinth Manivannan + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +use actix_web::{http::header::ContentType, web, HttpRequest, HttpResponse, Responder}; +use serde::{Serialize, Deserialize}; +use tokio::fs; + +use crate::errors::*; +use crate::pages; +use crate::AppCtx; + +pub mod routes { + use serde::Serialize; + + #[derive(Serialize)] + pub struct Serve { + pub catch_all: &'static str, + } + + impl Serve { + pub const fn new() -> Self { + Self { + catch_all: "/archive", + } + } + } +} + +#[derive(Serialize, Deserialize)] +struct Q { + url: url::Url, +} + +#[actix_web_codegen_const_routes::get(path = "crate::pages::PAGES.serve.catch_all")] +#[tracing::instrument(name = "Serve webpages", skip(ctx, id, q))] +async fn serve_webpage(q: web::Query<(Q)>, ctx: AppCtx, id: Identity) -> ServiceResult { + let url = q.into_inner().url; + + + let id = id.identity().unwrap(); + if ctx.db.url_exists(url.as_str()).await? { + let path = crate::utils::get_website_path(&ctx.settings, &id, &url); + let content = fs::read(&path).await?; + let mime = if let Some(mime) = mime_guess::from_path(&path).first_raw() { + mime + } else { + "text/html; charset=utf-8" + }; + + Ok(HttpResponse::Ok() + .content_type(mime) + .body(content)) + } else { + Err(ServiceError::WebsiteNotFound) + } +} + +pub fn services(cfg: &mut web::ServiceConfig) { + cfg.service(serve_webpage); +} diff --git a/src/settings.rs b/src/settings.rs new file mode 100644 index 0000000..5f7a5f5 --- /dev/null +++ b/src/settings.rs @@ -0,0 +1,170 @@ +/* + * Copyright (C) 2022 Aravinth Manivannan + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +use std::env; +use std::path::Path; + +use config::{Config, ConfigError, Environment, File}; +use derive_more::Display; +#[cfg(not(test))] +use tracing::warn; + +#[cfg(test)] +use std::println as warn; + +use serde::Deserialize; +use serde::Serialize; +use url::Url; + +use crate::errors::*; + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct Server { + pub port: u32, + pub ip: String, + pub workers: Option, + pub cookie_secret: String, + pub domain: String, +} + +#[derive(Deserialize, Serialize, Display, Eq, PartialEq, Clone, Debug)] +pub struct Pages { + pub base_path: String, +} + +impl Server { + #[cfg(not(tarpaulin_include))] + pub fn get_ip(&self) -> String { + format!("{}:{}", self.ip, self.port) + } +} + +#[derive(Deserialize, Serialize, Display, Eq, PartialEq, Clone, Debug)] +#[serde(rename_all = "lowercase")] +pub enum DBType { + #[display(fmt = "sqlite")] + Sqlite, + // #[display(fmt = "maria")] + // Maria, +} + +impl DBType { + fn from_url(url: &str) -> Result { + Ok(Self::Sqlite) + } +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct Database { + pub url: String, + pub pool: u32, + pub database_type: DBType, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct Settings { + pub allow_registration: bool, + pub support_email: String, + pub debug: bool, + pub server: Server, + pub source_code: String, + pub pages: Pages, + pub database: Database, +} + +#[cfg(not(tarpaulin_include))] +impl Settings { + pub fn new() -> ServiceResult { + let mut s = Config::builder(); + + const CURRENT_DIR: &str = "./config/default.toml"; + const ETC: &str = "/etc/pativu/config.toml"; + + let mut read_file = false; + + if Path::new(ETC).exists() { + s = s.add_source(File::with_name(ETC)); + read_file = true; + } + if Path::new(CURRENT_DIR).exists() { + // merging default config from file + s = s.add_source(File::with_name(CURRENT_DIR)); + read_file = true; + } + + if let Ok(path) = env::var("PATIVU_CONFIG") { + s = s.add_source(File::with_name(&path)); + read_file = true; + } + + if !read_file { + warn!("configuration file not found"); + } + + s = s.add_source(Environment::with_prefix("PATIVU").separator("__")); + + match env::var("PORT") { + Ok(val) => { + s = s.set_override("server.port", val).unwrap(); + } + Err(e) => warn!("couldn't interpret PORT: {}", e), + } + + let intermediate_config = s.build_cloned().unwrap(); + + s = s + .set_override( + "database.url", + format!( + r"sqlite://{}:{}@{}:{}/{}", + intermediate_config + .get::("database.username") + .expect("Couldn't access database username"), + intermediate_config + .get::("database.password") + .expect("Couldn't access database password"), + intermediate_config + .get::("database.hostname") + .expect("Couldn't access database hostname"), + intermediate_config + .get::("database.port") + .expect("Couldn't access database port"), + intermediate_config + .get::("database.name") + .expect("Couldn't access database name") + ), + ) + .expect("Couldn't set database url"); + + if let Ok(val) = env::var("DATABASE_URL") { + let database_type = DBType::from_url(&val).unwrap(); + s = s.set_override("database.url", val).unwrap(); + s = s + .set_override("database.database_type", database_type.to_string()) + .unwrap(); + } + + let settings = s.build()?.try_deserialize::()?; + settings.check_url(); + + Ok(settings) + } + + #[cfg(not(tarpaulin_include))] + fn check_url(&self) { + Url::parse(&self.source_code).expect("Please enter a URL for source_code in settings"); + } +} diff --git a/src/static_assets/filemap.rs b/src/static_assets/filemap.rs new file mode 100644 index 0000000..cd8cd29 --- /dev/null +++ b/src/static_assets/filemap.rs @@ -0,0 +1,46 @@ +/* + * Copyright (C) 2022 Aravinth Manivannan + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +use cache_buster::Files; + +pub struct FileMap { + pub files: Files, +} + +impl FileMap { + #[allow(clippy::new_without_default)] + pub fn new() -> Self { + let map = include_str!("../cache_buster_data.json"); + let files = Files::new(map); + Self { files } + } + pub fn get(&self, path: impl AsRef) -> Option<&str> { + let file_path = self.files.get_full_path(path); + file_path.map(|file_path| &file_path[1..]) + } +} + +#[cfg(test)] +mod tests { + + #[test] + fn filemap_works() { + let files = super::FileMap::new(); + let css = files.get("./static/cache/css/main.css").unwrap(); + println!("{}", css); + assert!(css.contains("/assets/css/main")); + } +} diff --git a/src/static_assets/mod.rs b/src/static_assets/mod.rs new file mode 100644 index 0000000..cfa45d4 --- /dev/null +++ b/src/static_assets/mod.rs @@ -0,0 +1,56 @@ +/* + * Copyright (C) 2022 Aravinth Manivannan + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +use actix_web::*; + +pub mod filemap; +pub mod static_files; + +pub use filemap::FileMap; +pub use routes::{Assets, ASSETS}; + +pub fn services(cfg: &mut web::ServiceConfig) { + cfg.service(static_files::static_files); +} + +pub mod routes { + use lazy_static::lazy_static; + use serde::*; + + use super::*; + + lazy_static! { + pub static ref ASSETS: Assets = Assets::new(); + } + + #[derive(Serialize)] + /// Top-level routes data structure for V1 AP1 + pub struct Assets { + /// Authentication routes + pub css: &'static str, + pub mobile_css: &'static str, + } + + impl Assets { + /// create new instance of Routes + pub fn new() -> Assets { + Assets { + css: &static_files::assets::CSS, + mobile_css: &static_files::assets::CSS, + } + } + } +} diff --git a/src/static_assets/static_files.rs b/src/static_assets/static_files.rs new file mode 100644 index 0000000..8565556 --- /dev/null +++ b/src/static_assets/static_files.rs @@ -0,0 +1,94 @@ +/* + * Copyright (C) 2022 Aravinth Manivannan + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +use std::borrow::Cow; + +use actix_web::body::BoxBody; +use actix_web::{get, http::header, web, HttpResponse, Responder}; +use mime_guess::from_path; +use rust_embed::RustEmbed; + +use crate::CACHE_AGE; + +pub mod assets { + use crate::FILES; + use lazy_static::lazy_static; + + lazy_static! { + pub static ref CSS: &'static str = FILES.get("./static/cache/css/main.css").unwrap(); + pub static ref MOBILE_CSS: &'static str = + FILES.get("./static/cache/css/mobile.css").unwrap(); + } +} + +#[derive(RustEmbed)] +#[folder = "assets/"] +struct Asset; + +fn handle_assets(path: &str) -> HttpResponse { + match Asset::get(path) { + Some(content) => { + let body: BoxBody = match content.data { + Cow::Borrowed(bytes) => BoxBody::new(bytes), + Cow::Owned(bytes) => BoxBody::new(bytes), + }; + + HttpResponse::Ok() + .insert_header(header::CacheControl(vec![ + header::CacheDirective::Public, + header::CacheDirective::Extension("immutable".into(), None), + header::CacheDirective::MaxAge(CACHE_AGE), + ])) + .content_type(from_path(path).first_or_octet_stream().as_ref()) + .body(body) + } + None => HttpResponse::NotFound().body("404 Not Found"), + } +} + +#[get("/assets/{_:.*}")] +pub async fn static_files(path: web::Path) -> impl Responder { + handle_assets(&path) +} + +#[cfg(test)] +mod tests { + use actix_web::http::StatusCode; + use actix_web::test; + + use crate::ctx::ArcCtx; + use crate::tests::*; + use crate::*; + + use super::assets::CSS; + use super::assets::MOBILE_CSS; + + #[actix_rt::test] + async fn postgrest_static_files_works() { + let (_, ctx) = get_ctx().await; + static_assets_work(ctx).await; + } + + async fn static_assets_work(ctx: ArcCtx) { + let app = get_app!(ctx).await; + + for file in [*CSS, *MOBILE_CSS].iter() { + println!("testing file {file}"); + let resp = get_request!(&app, file); + assert_eq!(resp.status(), StatusCode::OK); + } + } +} diff --git a/src/tests.rs b/src/tests.rs new file mode 100644 index 0000000..af73582 --- /dev/null +++ b/src/tests.rs @@ -0,0 +1,274 @@ +/* + * Copyright (C) 2022 Aravinth Manivannan + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +use std::sync::Arc; + +use actix_web::{ + body::{BoxBody, EitherBody}, + dev::ServiceResponse, + error::ResponseError, + http::StatusCode, +}; +use mktemp::Temp; +use serde::Serialize; + +use crate::ctx::api::v1::auth::{Login, Register}; +use crate::ctx::api::v1::pages::AddSite; +use crate::ctx::Ctx; +use crate::errors::*; +use crate::page::Page; +use crate::settings::Settings; +use crate::*; + +pub async fn get_ctx() -> (Temp, Arc) { + // mktemp::Temp is returned because the temp directory created + // is removed once the variable goes out of scope + let settings = Settings::new().unwrap(); + + let tmp_dir = Temp::new_dir().unwrap(); + println!("[log] Test temp directory: {}", tmp_dir.to_str().unwrap()); + let page_base_path = tmp_dir.as_path().join("base_path"); + println!("[log] Initialzing settings again with test config"); + + (tmp_dir, Ctx::new(settings).await) +} + +#[allow(dead_code, clippy::upper_case_acronyms)] +pub struct FORM; + +#[macro_export] +macro_rules! post_request { + ($uri:expr) => { + actix_web::test::TestRequest::post().uri($uri) + }; + + ($serializable:expr, $uri:expr) => { + actix_web::test::TestRequest::post() + .uri($uri) + .insert_header((actix_web::http::header::CONTENT_TYPE, "application/json")) + .set_payload(serde_json::to_string($serializable).unwrap()) + }; + + ($serializable:expr, $uri:expr, FORM) => { + actix_web::test::TestRequest::post() + .uri($uri) + .set_form($serializable) + }; +} + +#[macro_export] +macro_rules! get_request { + ($app:expr,$route:expr ) => { + test::call_service(&$app, test::TestRequest::get().uri($route).to_request()).await + }; + + ($app:expr, $route:expr, $cookies:expr) => { + test::call_service( + &$app, + test::TestRequest::get() + .uri($route) + .cookie($cookies) + .to_request(), + ) + .await + }; +} + +#[macro_export] +macro_rules! delete_request { + ($app:expr,$route:expr ) => { + test::call_service(&$app, test::TestRequest::delete().uri($route).to_request()).await + }; + + ($app:expr, $route:expr, $cookies:expr) => { + test::call_service( + &$app, + test::TestRequest::delete() + .uri($route) + .cookie($cookies) + .to_request(), + ) + .await + }; +} + +#[macro_export] +macro_rules! get_app { + ($ctx:expr) => { + actix_web::test::init_service( + actix_web::App::new() + .app_data($crate::get_json_err()) + .wrap($crate::get_identity_service(&$ctx.settings)) + .wrap(actix_web::middleware::NormalizePath::new( + actix_web::middleware::TrailingSlash::Trim, + )) + .configure($crate::services) + .app_data($crate::WebData::new($ctx.clone())), + ) + }; +} + +/// Utility function to check for status of a test response, attempt response payload serialization +/// and print payload if response status doesn't match expected status +#[macro_export] +macro_rules! check_status { + ($resp:expr, $expected:expr) => { + let status = $resp.status(); + if status != $expected { + eprintln!( + "[error] Expected status code: {} received: {status}", + $expected + ); + let response: serde_json::Value = actix_web::test::read_body_json($resp).await; + eprintln!("[error] Body:\n{:#?}", response); + assert_eq!(status, $expected); + panic!() + } + { + assert_eq!(status, $expected); + } + }; +} + +#[macro_export] +macro_rules! get_cookie { + ($resp:expr) => { + $resp.response().cookies().next().unwrap().to_owned() + }; +} + +impl Ctx { + /// register and signin utility + pub async fn register_and_signin( + &self, + name: &str, + email: &str, + password: &str, + ) -> (Login, ServiceResponse>) { + self.register_test(name, email, password).await; + self.signin_test(name, password).await + } + + pub fn to_arc(&self) -> Arc { + Arc::new(self.clone()) + } + + /// register utility + pub async fn register_test(&self, name: &str, email: &str, password: &str) { + let app = get_app!(self.to_arc()).await; + + // 1. Register + let msg = Register { + username: name.into(), + password: password.into(), + confirm_password: password.into(), + email: email.into(), + }; + println!("{:?}", msg); + let resp = actix_web::test::call_service( + &app, + post_request!(&msg, crate::V1_API_ROUTES.auth.register).to_request(), + ) + .await; + if resp.status() != StatusCode::OK { + let resp_err: ErrorToResponse = actix_web::test::read_body_json(resp).await; + panic!("{}", resp_err.error); + } + } + + /// signin util + pub async fn signin_test( + &self, + + name: &str, + password: &str, + ) -> (Login, ServiceResponse>) { + let app = get_app!(self.to_arc()).await; + + // 2. signin + let creds = Login { + login: name.into(), + password: password.into(), + }; + let signin_resp = actix_web::test::call_service( + &app, + post_request!(&creds, V1_API_ROUTES.auth.login).to_request(), + ) + .await; + assert_eq!(signin_resp.status(), StatusCode::OK); + (creds, signin_resp) + } + + /// pub duplicate test + pub async fn bad_post_req_test( + &self, + + name: &str, + password: &str, + url: &str, + payload: &T, + err: ServiceError, + ) { + let (_, signin_resp) = self.signin_test(name, password).await; + let cookies = get_cookie!(signin_resp); + let app = get_app!(self.to_arc()).await; + + let resp = actix_web::test::call_service( + &app, + post_request!(&payload, url) + .cookie(cookies.clone()) + .to_request(), + ) + .await; + assert_eq!(resp.status(), err.status_code()); + let resp_err: ErrorToResponse = actix_web::test::read_body_json(resp).await; + //println!("{}", txt.error); + assert_eq!(resp_err.error, format!("{}", err)); + } + + /// bad post req test without payload + pub async fn bad_post_req_test_witout_payload( + &self, + name: &str, + password: &str, + url: &str, + err: ServiceError, + ) { + let (_, signin_resp) = self.signin_test(name, password).await; + let app = get_app!(self.to_arc()).await; + let cookies = get_cookie!(signin_resp); + + let resp = actix_web::test::call_service( + &app, + post_request!(url).cookie(cookies.clone()).to_request(), + ) + .await; + assert_eq!(resp.status(), err.status_code()); + let resp_err: ErrorToResponse = actix_web::test::read_body_json(resp).await; + //println!("{}", resp_err.error); + assert_eq!(resp_err.error, format!("{}", err)); + } + + pub async fn add_test_site(&self, owner: String) -> Page { + unimplemented!() + // let msg = AddSite { + // repo_url: REPO_URL.into(), + // branch: BRANCH.into(), + // owner, + // }; + // self.add_site(msg).await.unwrap() + } +} diff --git a/src/utils.rs b/src/utils.rs new file mode 100644 index 0000000..8715a01 --- /dev/null +++ b/src/utils.rs @@ -0,0 +1,42 @@ +/* + * Copyright (C) 2022 Aravinth Manivannan + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +use crate::Settings; +use std::path::{Path, PathBuf}; + +use url::Url; + +/// Get random string of specific length +pub(crate) fn get_random(len: usize) -> String { + use rand::{distributions::Alphanumeric, rngs::ThreadRng, thread_rng, Rng}; + use std::iter; + + let mut rng: ThreadRng = thread_rng(); + + iter::repeat(()) + .map(|()| rng.sample(Alphanumeric)) + .map(char::from) + .take(len) + .collect::() +} + +pub(crate) fn get_website_path(s: &Settings, username: &str, url: &Url) -> PathBuf { + let path = url.as_str().replace('/', "-"); + + Path::new(&s.pages.base_path) + .join(username) + .join(path) +} diff --git a/static/cache/css/main.css b/static/cache/css/main.css new file mode 100644 index 0000000..bbaaf5e --- /dev/null +++ b/static/cache/css/main.css @@ -0,0 +1,423 @@ +* { + padding: 0; + margin: 0; + font-family: "Segoe UI", Tahoma, Geneva, Verdana, sans-serif; + font-family: "Segoe UI", Tahoma, Geneva, Verdana, sans-serif; +} + +a { + text-decoration: none; +} + +a:hover, button:hover { + cursor: pointer; +} + +a, +a:visited { + color: rgb(0, 86, 179); +} + +.base { + min-height: 100vh; + display: flex; + flex-direction: column; + width: 100%; +} + +.main__content-container { + display: flex; + flex-direction: column; + min-height: 100%; + justify-content: space-between; + flex: 2; +} + +p, +h1, +h2, +h3, +h4, +li, +ol, +ul { + color: #333; +} + +main { + width: 100%; +} + +blockquote { + border-left: 0.3em solid rgba(55, 55, 55, 0.4); + margin-bottom: 16px; + padding: 0 1em; + color: #707070; +} +blockquote p, +blockquote h1, +blockquote h2, +blockquote h3, +blockquote h4, +blockquote li, +blockquote ol, +blockquote ul { + color: inherit; +} + +.auth__body { + display: flex; + height: 100vh; + min-height: 500px; + max-height: 800px; + flex-direction: column; + justify-content: space-between; +} + +.index-banner__container { + width: 100%; + display: flex; + font-family: "Segoe UI", Tahoma, Geneva, Verdana, sans-serif; + flex-grow: 1; +} + +.index-banner { + margin: auto; + display: flex; + justify-content: space-between; +} + +.index-banner__logo-container { + margin: auto; + align-items: center; + display: flex; + flex-direction: column; + width: 500px; +} + +.index-banner__title { + margin: auto; + font-style: none; +} + +.index-banner__tagline { + margin: auto; +} + +.index-banner__title-container { + display: flex; +} + +.index-banner__logo { + width: 120px; + margin: auto; + border-radius: 20px; +} + +.index-banner__main-action-btn { + display: block; + display: block; + font-weight: 400; + padding: 15px; + border: none; + margin: 20px 0; + background-color: green; +} + +.index-banner__main-action-link { + color: white !important; +} + +.index-banner__features-list { + margin: 20px; +} + +.index-banner__features { + margin: 10px 0; +} + +.home__features { + display: flex; + flex-direction: column; + align-items: center; +} + +.home__features-title { + margin: auto; +} + +.index__group-content .page__container { + width: 80%; + height: 100vh; + min-height: 500px; + max-height: 800px; + height: 90vh !important; + display: flex; + flex-direction: column; + justify-content: space-around; +} + +.action-call__container { + background: #1f5818; + width: 100%; + padding: 60px 0; +} + +.action-call__margin-container { + display: flex; + width: 80%; + margin: auto; + align-items: center; + justify-content: space-around; +} + +.action-call__prompt { + color: white; + font-weight: 400; + font-size: 1.7rem; +} + +.action-call__button { + display: block; + display: block; + font-weight: 400; + padding: 15px; + border: none; + margin: 20px 0; + background-color: #fff; +} + +.action-call__button:hover { + background-color: lightgray; +} + +.action-call_link { + color: #000 !important; +} + +.action-call_link:hover { + text-decoration: none !important; +} + +.auth-form { + display: flex; + flex-direction: column; + width: 80%; + margin: auto; + padding: 0 10px; +} + +.auth-form__input { + display: block; + width: 100%; + margin: 10px 0; + padding: 5px 0; +} + +.auth-form__submit { + width: 100%; + display: block; + margin: 10px 0; + background-color: green; + color: #fff; + border: none; + padding: 5px 0; + cursor: pointer; +} + +.auth-form__submit:hover { + background-color: green; +} + +footer { + display: block; + color: #333; + font-size: 0.7rem; + padding: 0; + margin: 0; +} + +.footer__container { + width: 100%; + padding: 0; + justify-content: space-between; + margin: auto; + display: flex; + flex-direction: row; + overflow: hidden; +} + +.footer__column { + list-style: none; + display: flex; + margin: auto 50px; + align-items: center; + flex: 2.5; +} + +.footer__column--center { + list-style: none; + display: flex; + margin: auto 50px; + align-items: center; + flex: 2.5; + margin: auto; + flex-direction: column; + align-items: center; + flex: 2; +} + +.footer__column:last-child { + justify-content: flex-end; +} +.footer__column:last-child a { + margin: 10px; +} + +.footer__link-container { + margin: 5px; +} + +.footer__link { + text-decoration: none; +} + +.license__link { + display: inline; +} + +.license__link:hover { + color: rgb(0, 86, 179); + text-decoration: underline; +} + +.footer__column-divider, +.footer__column-divider--mobile-visible, +.footer__column-divider--mobile-only { + font-weight: 500; + opacity: 0.7; + margin: 0 5px; +} + +.footer__column-divider--mobile-only { + display: none; +} + +.footer__icon { + margin: auto 5px; + height: 20px; +} + +header { + z-index: 5; + position: sticky; + top: 0; + background-color: #fff; +} + +.nav__container { + display: flex; + flex-direction: row; + box-sizing: border-box; + width: 100%; + padding-top: 5px; + border-bottom: 1px solid rgb(211, 211, 211); +} + +.nav__home-btn { + font-weight: bold; + margin: auto; + margin-left: 10px; +} + +.nav__hamburger-menu { + display: none; +} + +.nav__spacer--small { + width: 100px; + margin: auto; +} + +.nav__spacer { + flex: 4; + margin: auto; +} + +.nav__logo-container { + display: inline-flex; + text-decoration: none; +} + +.nav__logo-container:hover { + color: rgb(0, 86, 179); + text-decoration: underline; +} + +.nav__toggle { + display: none; +} + +.nav__logo { + display: inline-flex; + margin: auto; + padding: 5px; + width: 40px; +} + +.nav__link-group { + flex: 1.5; + list-style: none; + display: flex; + flex-direction: row; + align-items: center; + align-self: center; + margin: auto; + text-align: center; +} + +.nav__link-group--small { + flex: 1.5; + list-style: none; + display: flex; + flex-direction: row; + align-items: center; + align-self: center; + margin: auto; + text-align: center; + flex: 0.5; + margin-right: 10px; +} + +.nav__link-container { + display: flex; + padding: 10px; + height: 100%; + margin: auto; +} + +.nav__link-container--action { + display: flex; + padding: 10px; + height: 100%; + margin: auto; + background-color: green; + padding: 15px; +} +.nav__link-container--action .nav__link { + color: white !important; +} + +.nav__link { + text-decoration: none; + color: black !important; + font-weight: 600; + font-size: 14px; +} + +.nav__link:hover { + color: rgb(0, 86, 179); + text-decoration: underline; +} + +/*# sourceMappingURL=main.css.map */ diff --git a/static/cache/css/main.css.map b/static/cache/css/main.css.map new file mode 100644 index 0000000..f0af16a --- /dev/null +++ b/static/cache/css/main.css.map @@ -0,0 +1 @@ +{"version":3,"sourceRoot":"","sources":["../../../templates/defaults.scss","../../../templates/pages/auth/sass/main.scss","../../../templates/components/sass/_fullscreen.scss","../../../templates/pages/auth/sass/form/main.scss","../../../templates/components/sass/footer/main.scss","../../../templates/components/sass/_link.scss","../../../templates/components/nav/sass/main.scss"],"names":[],"mappings":"AAAA;EACC;EACA;EAGA;EACA;;;AAGD;EACC;;;AAGD;EACC;;;AAGD;AAAA;EAEC;;;AAGD;EACC;EACA;EACA;EACA;;;AAGD;EACC;EACA;EACA;EACA;EACA;;;AAGD;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;EAQC;;;AAGD;EACC;;;AAGD;EACC;EACA;EAEA;EACA;;AAEA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;EAQC;;;ACjEF;EACC;ECFA;EACA;EACA;EDEA;EACA;;;AAID;EACC;EACA;EAIA;EACA;;;AAGD;EACC;EACA;EAEA;;;AASD;EACC;EACA;EACA;EACA;EACA;;;AAGD;EACC;EACA;;;AAID;EACC;;;AAKD;EACC;;;AAGD;EACC;EACA;EACA;;;AAGD;EACC;EACA;EACA;EACA;EACA;EACA;EACA;;;AAGD;EACC;;;AAOD;EACC;;;AAGD;EACC;;;AAGD;EACC;EACA;EACA;;;AAGD;EACC;;;AAKA;EACC,OAHmB;EClGpB;EACA;EACA;EDqGC;EACA;EACA;EACA;;;AAIF;EACC;EACA;EACA;;;AAGD;EACC;EACA,OApBoB;EAqBpB;EACA;EACA;;;AAGD;EACC;EACA;EACA;;;AAGD;EACC;EACA;EACA;EACA;EACA;EACA;EACA;;;AAGD;EACC;;;AAGD;EACC;;;AAGD;EACC;;;AEtJD;EACC;EACA;EACA;EACA;EACA;;;AAGD;EACC;EACA;EACA;EACA;;;AAGD;EACC;EACA;EACA;EACA;EACA;EACA;EACA;EACA;;;AAGD;EACC;;;ACzBD;EACC;EACA;EACA;EACA;EACA;;;AAGD;EACC;EACA;EACA;EACA;EACA;EACA;EACA;;;AAWD;EAPC;EACA;EACA;EACA;EACA;;;AAOD;EAXC;EACA;EACA;EACA;EACA;EASA;EACA;EACA;EACA;;;AAGD;EACC;;AACA;EACC;;;AAIF;EACC;;;AAGD;EACC;;;AAGD;EACC;;;AAGD;EC1DC;EACA;;;AD6DD;AAAA;AAAA;EAGC;EACA;EACA;;;AAGD;EACC;;;AAGD;EACC;EACA;;;AE3ED;EACC;EACA;EACA;EACA;;;AAGD;EACC;EACA;EAEA;EACA;EACA;EACA;;;AAGD;EACC;EAEA;EACA;;;AAGD;EACC;;;AAGD;EACC;EACA;;;AAGD;EACC;EACA;;;AAGD;EACC;EACA;;;AAGD;ED5CC;EACA;;;AC+CD;EACC;;;AAGD;EACC;EACA;EACA;EACA;;;AAcD;EAVC;EACA;EACA;EACA;EACA;EACA;EACA;EACA;;;AAQD;EAfC;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EAUA;EACA;;;AAUD;EANC;EACA;EACA;EACA;;;AAOD;EAVC;EACA;EACA;EACA;EASA;EACA;;AACA;EACC;;;AAIF;EACC;EACA;EACA;EACA;;;AAGD;ED5GC;EACA","file":"main.css"} \ No newline at end of file diff --git a/static/cache/css/mobile.css b/static/cache/css/mobile.css new file mode 100644 index 0000000..1504f2d --- /dev/null +++ b/static/cache/css/mobile.css @@ -0,0 +1,209 @@ +footer { + font-size: 0.44rem; +} + +.footer__container { + display: grid; + grid-template-rows: repeat(3, 100%); + align-items: center; + margin: auto; + justify-content: center; +} + +.footer__link { + font-size: 0.5rem; +} + +.license__conatiner, +.license__link { + text-align: center; +} + +.footer__column:first-child { + grid-row-start: 3; + flex-direction: row; +} + +.footer__column:last-child { + grid-row-start: 2; +} + +.footer__column { + margin: 0 auto; + display: flex; + padding: 0; + align-self: flex-end; +} + +.footer__column--center { + margin: 0 auto; + display: flex; + padding: 0; + align-self: flex-start; +} + +.footer__column-divider--mobile-only { + margin: 0 3px; + font-size: 9.9px; +} + +.home__container { + max-height: 100vh; + height: 100vh; +} + +.home__name { + font-size: 2rem; +} + +.index-banner { + margin: auto; +} + +.index-banner__title { + font-size: 2.5rem; + margin: auto; +} + +.index__group-content .page__container { + width: 90%; +} + +.index-banner__logo-container { + display: none; +} + +.action-call__margin-container { + flex-direction: column; + width: 85%; +} + +.action-call__prompt { + text-align: center; +} + +.nav__container { + flex-direction: column; +} + +.nav__header { + display: flex; + flex-direction: row; + min-width: 100%; + justify-content: space-between; +} + +.nav__link-group, +.nav__link-group--small { + position: sticky; + flex-direction: column; + margin: auto; + align-items: center; + width: 100%; +} + +.nav__link-container--action { + background-color: #fff; +} +.nav__link-container--action .nav__link { + color: #000 !important; +} + +.nav__link-container { + border-bottom: 1px dashed rgba(55, 55, 55, 0.4); + width: 70%; +} + +.nav__link-container--action { + border-bottom: 1px dashed rgba(55, 55, 55, 0.4); + width: 70%; +} + +.nav__link-container:last-child { + border-bottom: none; +} + +.nav__link { + margin: auto; +} + +.nav__hamburger-menu { + display: inline-block; + width: 50px; + height: 50px; +} + +.nav__spacer { + display: none; +} + +.nav__link-group { + margin-right: auto; +} + +.nav__toggle:not(:checked) ~ .nav__link-group, .nav__link-group--small { + max-height: 0; + transition: max-height 0.4s ease-out; + overflow: hidden; +} + +.nav__toggle:checked ~ .nav__link-group, .nav__toggle:checked ~ .nav__link-group--small { + max-height: 500px; + transition: max-height 0.4s ease-out; +} + +.nav__toggle:checked ~ .nav__header .nav__hamburger-inner::after { + width: 24px; + bottom: 1.3px; + transform: rotate(-90deg); + transition: bottom 0.1s ease-out, transform 0.22s cubic-bezier(0.215, 0.61, 0.355, 1) 0.12s, width 0.1s ease-out; +} +.nav__toggle:checked ~ .nav__header .nav__hamburger-inner::before { + top: 0; + opacity: 0; + transition: top 0.1s ease-out, opacity 0.1s ease-out 0.12s; +} +.nav__toggle:checked ~ .nav__header .nav__hamburger-inner { + transform: rotate(225deg); + transition-delay: 0.12s; + transition-timing-function: cubic-bezier(0.215, 0.61, 0.355, 1); +} + +.nav__hamburger-inner::after { + bottom: -7px; + transition: bottom 0.1s ease-in 0.25s, transform 0.22s cubic-bezier(0.55, 0.055, 0.675, 0.19), width 0.1s ease-in 0.25s; +} + +.nav__hamburger-inner::after, +.nav__hamburger-inner::before { + content: ""; + display: block; +} + +.nav__hamburger-inner::before { + top: -7px; + transition: top 0.1s ease-in 0.25s, opacity 0.1s ease-in; +} + +.nav__hamburger-inner { + top: 50%; + margin: auto; + transition-duration: 0.22s; + transition-timing-function: cubic-bezier(0.55, 0.055, 0.675, 0.19); +} + +.nav__hamburger-inner, +.nav__hamburger-inner::after, +.nav__hamburger-inner::before { + width: 24px; + height: 1.3px; + position: relative; + background: #000; +} + +.nav__hamburger-menu, +.nav__hamburger-inner { + display: block; +} + +/*# sourceMappingURL=mobile.css.map */ diff --git a/static/cache/css/mobile.css.map b/static/cache/css/mobile.css.map new file mode 100644 index 0000000..0fc7bb1 --- /dev/null +++ b/static/cache/css/mobile.css.map @@ -0,0 +1 @@ +{"version":3,"sourceRoot":"","sources":["../../../templates/components/sass/footer/mobile.scss","../../../templates/pages/auth/sass/mobile.scss","../../../templates/components/nav/sass/mobile.scss"],"names":[],"mappings":"AAEA;EACC,WAHkB;;;AAMnB;EACC;EACA;EACA;EACA;EACA;;;AAGD;EACC;;;AAGD;AAAA;EAEC;;;AASD;EACC;EACA;;;AAGD;EACC;;;AAGD;EAdC;EACA;EACA;EAcA;;;AAGD;EAnBC;EACA;EACA;EAmBA;;;AAGD;EACC;EACA;;;AClDD;EACC;EACA;;;AAGD;EACC;;;AAGD;EACC;;;AAGD;EACC;EACA;;;AAIA;EACC;;;AAIF;EACC;;;AAGD;EACC;EACA;;;AAGD;EACC;;;AC7BD;EACC;;;AAGD;EACC;EACA;EACA;EACA;;;AAGD;AAAA;EAEC;EACA;EACA;EACA;EACA;;;AAID;EACC;;AACA;EACC;;;AASF;EAJC;EACA;;;AAOD;EARC;EACA;;;AAWD;EACC;;;AAGD;EACC;;;AAGD;EACC;EACA;EACA;;;AAGD;EACC;;;AAGD;EACC;;;AAGD;EACC;EACA;EACA;;;AAGD;EACC;EACA;;;AAIA;EACC;EACA,QA/E4B;EAgF5B;EACA;;AAKD;EACC;EACA;EACA;;AAGD;EACC;EACA;EACA;;;AAIF;EACC;EACA;;;AAKD;AAAA;EAEC;EACA;;;AAGD;EACC;EACA;;;AAGD;EACC;EACA;EACA;EACA;;;AAGD;AAAA;AAAA;EAGC;EACA,QAhI6B;EAiI7B;EAEA;;;AAGD;AAAA;EAEC","file":"mobile.css"} \ No newline at end of file diff --git a/templates/components/base.html b/templates/components/base.html new file mode 100644 index 0000000..d7a892d --- /dev/null +++ b/templates/components/base.html @@ -0,0 +1,15 @@ + + + + + + + Pativu + {% block title %} {% endblock %} + + +

{% block nav %} {% endblock %}
+ {% block main %} {% endblock %} + {% include "footer" %} + + diff --git a/templates/components/error.html b/templates/components/error.html new file mode 100644 index 0000000..bb36399 --- /dev/null +++ b/templates/components/error.html @@ -0,0 +1,6 @@ +{% if error %} +
+

ERROR: {{ error.title }}

+

{{ error.reason }}

+
+{% endif %} diff --git a/templates/components/footer.html b/templates/components/footer.html new file mode 100644 index 0000000..3b511b2 --- /dev/null +++ b/templates/components/footer.html @@ -0,0 +1,37 @@ + diff --git a/templates/components/nav/auth.html b/templates/components/nav/auth.html new file mode 100644 index 0000000..283f0fc --- /dev/null +++ b/templates/components/nav/auth.html @@ -0,0 +1,28 @@ + diff --git a/templates/components/nav/base.html b/templates/components/nav/base.html new file mode 100644 index 0000000..f095374 --- /dev/null +++ b/templates/components/nav/base.html @@ -0,0 +1,18 @@ + diff --git a/templates/components/nav/pub.html b/templates/components/nav/pub.html new file mode 100644 index 0000000..a916203 --- /dev/null +++ b/templates/components/nav/pub.html @@ -0,0 +1,23 @@ + diff --git a/templates/components/nav/sass/main.scss b/templates/components/nav/sass/main.scss new file mode 100644 index 0000000..baef4b3 --- /dev/null +++ b/templates/components/nav/sass/main.scss @@ -0,0 +1,112 @@ +@import "../../sass/_link"; + +header { + z-index: 5; + position: sticky; + top: 0; + background-color: #fff; +} + +.nav__container { + display: flex; + flex-direction: row; + + box-sizing: border-box; + width: 100%; + padding-top: 5px; + border-bottom: 1px solid rgb(211, 211, 211); +} + +.nav__home-btn { + font-weight: bold; + // font-family: monospace, monospace; + margin: auto; + margin-left: 10px; +} + +.nav__hamburger-menu { + display: none; +} + +.nav__spacer--small { + width: 100px; + margin: auto; +} + +.nav__spacer { + flex: 4; + margin: auto; +} + +.nav__logo-container { + display: inline-flex; + text-decoration: none; +} + +.nav__logo-container:hover { + @include a_hover; +} + +.nav__toggle { + display: none; +} + +.nav__logo { + display: inline-flex; + margin: auto; + padding: 5px; + width: 40px; +} + +@mixin nav__link-group { + flex: 1.5; + list-style: none; + display: flex; + flex-direction: row; + align-items: center; + align-self: center; + margin: auto; + text-align: center; +} + +.nav__link-group { + @include nav__link-group; + +} + +.nav__link-group--small { + @include nav__link-group; + flex: 0.5; + margin-right: 10px; +} + +@mixin nav__link-container { + display: flex; + padding: 10px; + height: 100%; + margin: auto; +} + +.nav__link-container { + @include nav__link-container; +} + +.nav__link-container--action { + @include nav__link-container; + background-color: green; + padding: 15px; + .nav__link { + color: white !important; + } +} + +.nav__link { + text-decoration: none; + color: black !important; + font-weight: 600; + font-size: 14px; +} + +.nav__link:hover { + @include a_hover; +} diff --git a/templates/components/nav/sass/mobile.scss b/templates/components/nav/sass/mobile.scss new file mode 100644 index 0000000..2f9040c --- /dev/null +++ b/templates/components/nav/sass/mobile.scss @@ -0,0 +1,141 @@ +//@import '../_vars'; + +$hamburger-menu-animation: 0.4s ease-out; +$nav__hamburger-inner-height: 1.3px; + +.nav__container { + flex-direction: column; +} + +.nav__header { + display: flex; + flex-direction: row; + min-width: 100%; + justify-content: space-between; +} + +.nav__link-group, +.nav__link-group--small { + position: sticky; + flex-direction: column; + margin: auto; + align-items: center; + width: 100%; + // background-color: $light-blue; +} + +.nav__link-container--action { + background-color: #fff; + .nav__link { + color: #000 !important; + } +} + +@mixin nav__link-container { + border-bottom: 1px dashed rgba(55, 55, 55, 0.4); + width: 70%; +} + +.nav__link-container { + @include nav__link-container; +} + +.nav__link-container--action { + @include nav__link-container; +} + +.nav__link-container:last-child { + border-bottom: none; +} + +.nav__link { + margin: auto; +} + +.nav__hamburger-menu { + display: inline-block; + width: 50px; + height: 50px; +} + +.nav__spacer { + display: none; +} + +.nav__link-group { + margin-right: auto; +} + +.nav__toggle:not(:checked) ~ .nav__link-group, .nav__link-group--small { + max-height: 0; + transition: max-height $hamburger-menu-animation; + overflow: hidden; +} + +.nav__toggle:checked ~ .nav__link-group, .nav__toggle:checked ~ .nav__link-group--small { + max-height: 500px; + transition: max-height $hamburger-menu-animation; +} + +.nav__toggle:checked ~ .nav__header { + .nav__hamburger-inner::after { + width: 24px; + bottom: $nav__hamburger-inner-height; + transform: rotate(-90deg); + transition: bottom 0.1s ease-out, + transform 0.22s cubic-bezier(0.215, 0.61, 0.355, 1) 0.12s, + width 0.1s ease-out; + } + + .nav__hamburger-inner::before { + top: 0; + opacity: 0; + transition: top 0.1s ease-out, opacity 0.1s ease-out 0.12s; + } + + .nav__hamburger-inner { + transform: rotate(225deg); + transition-delay: 0.12s; + transition-timing-function: cubic-bezier(0.215, 0.61, 0.355, 1); + } +} + +.nav__hamburger-inner::after { + bottom: -7px; + transition: bottom 0.1s ease-in 0.25s, + transform 0.22s cubic-bezier(0.55, 0.055, 0.675, 0.19), + width 0.1s ease-in 0.25s; +} + +.nav__hamburger-inner::after, +.nav__hamburger-inner::before { + content: ""; + display: block; +} + +.nav__hamburger-inner::before { + top: -7px; + transition: top 0.1s ease-in 0.25s, opacity 0.1s ease-in; +} + +.nav__hamburger-inner { + top: 50%; + margin: auto; + transition-duration: 0.22s; + transition-timing-function: cubic-bezier(0.55, 0.055, 0.675, 0.19); +} + +.nav__hamburger-inner, +.nav__hamburger-inner::after, +.nav__hamburger-inner::before { + width: 24px; + height: $nav__hamburger-inner-height; + position: relative; + // background: $dark-black; + background: #000; +} + +.nav__hamburger-menu, +.nav__hamburger-inner { + display: block; +} diff --git a/templates/components/sass/_fullscreen.scss b/templates/components/sass/_fullscreen.scss new file mode 100644 index 0000000..3f6c693 --- /dev/null +++ b/templates/components/sass/_fullscreen.scss @@ -0,0 +1,5 @@ +@mixin fullscreen { + height: 100vh; + min-height: 500px; + max-height: 800px; +} diff --git a/templates/components/sass/_link.scss b/templates/components/sass/_link.scss new file mode 100644 index 0000000..e957dba --- /dev/null +++ b/templates/components/sass/_link.scss @@ -0,0 +1,4 @@ +@mixin a_hover { + color: rgb(0, 86, 179); + text-decoration: underline; +} diff --git a/templates/components/sass/footer/main.scss b/templates/components/sass/footer/main.scss new file mode 100644 index 0000000..41de9d3 --- /dev/null +++ b/templates/components/sass/footer/main.scss @@ -0,0 +1,79 @@ +@import "../_link"; + +footer { + display: block; + color: #333; + font-size: 0.7rem; + padding: 0; + margin: 0; +} + +.footer__container { + width: 100%; + padding: 0; + justify-content: space-between; + margin: auto; + display: flex; + flex-direction: row; + overflow: hidden; +} + +@mixin footer__column-base { + list-style: none; + display: flex; + margin: auto 50px; + align-items: center; + flex: 2.5; +} + +.footer__column { + @include footer__column-base; +} + +.footer__column--center { + @include footer__column-base; + margin: auto; + flex-direction: column; + align-items: center; + flex: 2; +} + +.footer__column:last-child { + justify-content: flex-end; + a { + margin: 10px; + } +} + +.footer__link-container { + margin: 5px; +} + +.footer__link { + text-decoration: none; +} + +.license__link { + display: inline; +} + +.license__link:hover { + @include a_hover; +} + +.footer__column-divider, +.footer__column-divider--mobile-visible, +.footer__column-divider--mobile-only { + font-weight: 500; + opacity: 0.7; + margin: 0 5px; +} + +.footer__column-divider--mobile-only { + display: none; +} + +.footer__icon { + margin: auto 5px; + height: 20px; +} diff --git a/templates/components/sass/footer/mobile.scss b/templates/components/sass/footer/mobile.scss new file mode 100644 index 0000000..f6e0c1f --- /dev/null +++ b/templates/components/sass/footer/mobile.scss @@ -0,0 +1,52 @@ +$footer-font-size: 0.44rem; + +footer { + font-size: $footer-font-size; +} + +.footer__container { + display: grid; + grid-template-rows: repeat(3, 100%); + align-items: center; + margin: auto; + justify-content: center; +} + +.footer__link { + font-size: 0.5rem; +} + +.license__conatiner, +.license__link { + text-align: center; +} + +@mixin footer__column-base { + margin: 0 auto; + display: flex; + padding: 0; +} + +.footer__column:first-child { + grid-row-start: 3; + flex-direction: row; +} + +.footer__column:last-child { + grid-row-start: 2; +} + +.footer__column { + @include footer__column-base; + align-self: flex-end; +} + +.footer__column--center { + @include footer__column-base; + align-self: flex-start; +} + +.footer__column-divider--mobile-only { + margin: 0 3px; + font-size: 9.9px; +} diff --git a/templates/defaults.scss b/templates/defaults.scss new file mode 100644 index 0000000..4e38a60 --- /dev/null +++ b/templates/defaults.scss @@ -0,0 +1,70 @@ +* { + padding: 0; + margin: 0; + //font-family: "Inter UI", -apple-system, BlinkMacSystemFont, "Roboto", + // "Segoe UI", Helvetica, Arial, sans-serif; + font-family: "Segoe UI", Tahoma, Geneva, Verdana, sans-serif; + font-family: "Segoe UI", Tahoma, Geneva, Verdana, sans-serif; +} + +a { + text-decoration: none; +} + +a:hover, button:hover { + cursor: pointer; +} + +a, +a:visited { + color: rgb(0, 86, 179); +} + +.base { + min-height: 100vh; + display: flex; + flex-direction: column; + width: 100%; +} + +.main__content-container { + display: flex; + flex-direction: column; + min-height: 100%; + justify-content: space-between; + flex: 2; +} + +p, +h1, +h2, +h3, +h4, +li, +ol, +ul { + color: #333; +} + +main { + width: 100%; +} + +blockquote { + border-left: 0.3em solid rgba(55, 55, 55, 0.4); + margin-bottom: 16px; + //padding-left: 20px; + padding: 0 1em; + color: #707070; + + p, + h1, + h2, + h3, + h4, + li, + ol, + ul { + color: inherit; + } +} diff --git a/templates/main.scss b/templates/main.scss new file mode 100644 index 0000000..ea4c173 --- /dev/null +++ b/templates/main.scss @@ -0,0 +1,5 @@ +@import "defaults.scss"; +@import "pages/auth/sass/main.scss"; +@import "pages/auth/sass/form/main.scss"; +@import "components/sass/footer/main.scss"; +@import "components/nav/sass/main.scss"; diff --git a/templates/mobile.scss b/templates/mobile.scss new file mode 100644 index 0000000..ed8ebf1 --- /dev/null +++ b/templates/mobile.scss @@ -0,0 +1,3 @@ +@import "components/sass/footer/mobile.scss"; +@import "pages/auth/sass/mobile.scss"; +@import "components/nav/sass/mobile.scss"; diff --git a/templates/pages/auth/base.html b/templates/pages/auth/base.html new file mode 100644 index 0000000..913a953 --- /dev/null +++ b/templates/pages/auth/base.html @@ -0,0 +1,46 @@ + + + + + + + Pativu + + +
{% include "pub_nav" %}
+ +
+
+
+

Self-hosted internet archive

+

+ Personal internet archiving platform with focus on speed +

+
    +
  • + Sanitize webpages improving reader focus +
  • +
  • + Small, single-binary making self-hosting easy +
  • +
  • + Crawl linked static assets to completely archive a webpage +
  • +
  • + + 100% + Free Software : deploy your own instance +
  • +
+
+
+
+ {% block login %} {% endblock %} +
+
+ {% include "footer" %} + + diff --git a/templates/pages/auth/login.html b/templates/pages/auth/login.html new file mode 100644 index 0000000..bec8cc5 --- /dev/null +++ b/templates/pages/auth/login.html @@ -0,0 +1,44 @@ +{% extends 'authbase' %} +{% block login %} +

Sign In

+
+ {% include "error_comp" %} + + + +
+ Forgot password? + +
+
+ +

+ New to Pativu? + Create an account +

+{% endblock %} diff --git a/templates/pages/auth/register.html b/templates/pages/auth/register.html new file mode 100644 index 0000000..836e7d6 --- /dev/null +++ b/templates/pages/auth/register.html @@ -0,0 +1,73 @@ +{% extends 'authbase' %} +{% block title_name %}Sign Up {% endblock %} +{% block login %} +

Sign Up

+
+ {% include "error_comp" %} + + + + + + + +
+ Forgot password? + +
+
+ +

+ Already have an account? + Login +

+{% endblock %} diff --git a/templates/pages/auth/sass/form/main.scss b/templates/pages/auth/sass/form/main.scss new file mode 100644 index 0000000..a16f7e5 --- /dev/null +++ b/templates/pages/auth/sass/form/main.scss @@ -0,0 +1,29 @@ +.auth-form { + display: flex; + flex-direction: column; + width: 80%; + margin: auto; + padding: 0 10px; +} + +.auth-form__input { + display: block; + width: 100%; + margin: 10px 0; + padding: 5px 0; +} + +.auth-form__submit { + width: 100%; + display: block; + margin: 10px 0; + background-color: green; + color: #fff; + border: none; + padding: 5px 0; + cursor: pointer; +} + +.auth-form__submit:hover { + background-color: green; +} diff --git a/templates/pages/auth/sass/main.scss b/templates/pages/auth/sass/main.scss new file mode 100644 index 0000000..00fe4ed --- /dev/null +++ b/templates/pages/auth/sass/main.scss @@ -0,0 +1,152 @@ +@import "../../../components/sass/fullscreen"; + +.auth__body { + display: flex; + @include fullscreen; + flex-direction: column; + justify-content: space-between; +} + +$heading-letter-spacing: 20px; +.index-banner__container { + width: 100%; + display: flex; + //background-color: #d1875a; + // background-color: #3c3c3c; + // background-color: #58181f; + font-family: "Segoe UI", Tahoma, Geneva, Verdana, sans-serif; + flex-grow: 1; +} + +.index-banner { + margin: auto; + display: flex; + // flex-direction: column; + justify-content: space-between; +} + +.index-banner__content-container { + // height: 300px; + li { + // color: white; + } +} +.index-banner__logo-container { + margin: auto; + align-items: center; + display: flex; + flex-direction: column; + width: 500px; +} + +.index-banner__title { + margin: auto; + font-style: none; + //color: #fff; +} + +.index-banner__tagline { + margin: auto; + // color: #fff; + // font-size: 1.4rem; +} + +.index-banner__title-container { + display: flex; +} + +.index-banner__logo { + width: 120px; + margin: auto; + border-radius: 20px; +} + +.index-banner__main-action-btn { + display: block; + display: block; + font-weight: 400; + padding: 15px; + border: none; + margin: 20px 0; + background-color: green; +} + +.index-banner__main-action-link { + color: white !important; +} + +.index-banner__main-action-btn:hover { + // background-color: lightgray; +} + +.index-banner__features-list { + margin: 20px; +} + +.index-banner__features { + margin: 10px 0; +} + +.home__features { + display: flex; + flex-direction: column; + align-items: center; +} + +.home__features-title { + margin: auto; +} + +$page-content-width: 80%; +.index__group-content { + .page__container { + width: $page-content-width; + @include fullscreen; + height: 90vh !important; + display: flex; + flex-direction: column; + justify-content: space-around; + } +} + +.action-call__container { + background: #1f5818; + width: 100%; + padding: 60px 0; +} + +.action-call__margin-container { + display: flex; + width: $page-content-width; + margin: auto; + align-items: center; + justify-content: space-around; +} + +.action-call__prompt { + color: white; + font-weight: 400; + font-size: 1.7rem; +} + +.action-call__button { + display: block; + display: block; + font-weight: 400; + padding: 15px; + border: none; + margin: 20px 0; + background-color: #fff; +} + +.action-call__button:hover { + background-color: lightgray; +} + +.action-call_link { + color: #000 !important; +} + +.action-call_link:hover { + text-decoration: none !important; +} diff --git a/templates/pages/auth/sass/mobile.scss b/templates/pages/auth/sass/mobile.scss new file mode 100644 index 0000000..4d8e696 --- /dev/null +++ b/templates/pages/auth/sass/mobile.scss @@ -0,0 +1,36 @@ +.home__container { + max-height: 100vh; + height: 100vh; +} + +.home__name { + font-size: 2rem; +} + +.index-banner { + margin: auto; +} + +.index-banner__title { + font-size: 2.5rem; + margin: auto; +} + +.index__group-content { + .page__container { + width: 90%; + } +} + +.index-banner__logo-container { + display: none; +} + +.action-call__margin-container { + flex-direction: column; + width: 85%; +} + +.action-call__prompt { + text-align: center; +} diff --git a/templates/pages/dash/index.html b/templates/pages/dash/index.html new file mode 100644 index 0000000..bc5b245 --- /dev/null +++ b/templates/pages/dash/index.html @@ -0,0 +1,154 @@ + + + + + + + Pativu + + +
+ +
+
+
+ + {% for site in payload %} + +
+
+

{{ site.url }}

+
+
+
+ {% endfor %} +
+
+ {% include "footer" %} + + + + diff --git a/templates/pages/dash/sites/add.html b/templates/pages/dash/sites/add.html new file mode 100644 index 0000000..6d61fff --- /dev/null +++ b/templates/pages/dash/sites/add.html @@ -0,0 +1,144 @@ + + + + + + + Pativu + + +
+ +
+
+
+
+ + +
+ {% include "footer" %} + + + + diff --git a/utils/cache-bust/.gitignore b/utils/cache-bust/.gitignore new file mode 100644 index 0000000..67e326a --- /dev/null +++ b/utils/cache-bust/.gitignore @@ -0,0 +1,2 @@ +/target +src/cache_buster_data.json diff --git a/utils/cache-bust/Cargo.lock b/utils/cache-bust/Cargo.lock new file mode 100644 index 0000000..39cb601 --- /dev/null +++ b/utils/cache-bust/Cargo.lock @@ -0,0 +1,354 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 3 + +[[package]] +name = "block-buffer" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0bf7fe51849ea569fd452f37822f606a5cabb684dc918707a0193fd4664ff324" +dependencies = [ + "generic-array", +] + +[[package]] +name = "cache-buster" +version = "0.2.0" +source = "git+https://github.com/realaravinth/cache-buster#7ca4545722fb99be30698a5e72c7d982a70fa11f" +dependencies = [ + "data-encoding", + "derive_builder", + "mime", + "mime_guess", + "serde", + "serde_json", + "sha2", + "walkdir", +] + +[[package]] +name = "cfg-if" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" + +[[package]] +name = "cpufeatures" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "59a6001667ab124aebae2a495118e11d30984c3a653e99d86d58971708cf5e4b" +dependencies = [ + "libc", +] + +[[package]] +name = "crypto-common" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "57952ca27b5e3606ff4dd79b0020231aaf9d6aa76dc05fd30137538c50bd3ce8" +dependencies = [ + "generic-array", + "typenum", +] + +[[package]] +name = "darling" +version = "0.14.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4529658bdda7fd6769b8614be250cdcfc3aeb0ee72fe66f9e41e5e5eb73eac02" +dependencies = [ + "darling_core", + "darling_macro", +] + +[[package]] +name = "darling_core" +version = "0.14.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "649c91bc01e8b1eac09fb91e8dbc7d517684ca6be8ebc75bb9cafc894f9fdb6f" +dependencies = [ + "fnv", + "ident_case", + "proc-macro2", + "quote", + "strsim", + "syn", +] + +[[package]] +name = "darling_macro" +version = "0.14.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ddfc69c5bfcbd2fc09a0f38451d2daf0e372e367986a83906d1b0dbc88134fb5" +dependencies = [ + "darling_core", + "quote", + "syn", +] + +[[package]] +name = "data-encoding" +version = "2.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3ee2393c4a91429dffb4bedf19f4d6abf27d8a732c8ce4980305d782e5426d57" + +[[package]] +name = "derive_builder" +version = "0.11.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d07adf7be193b71cc36b193d0f5fe60b918a3a9db4dad0449f57bcfd519704a3" +dependencies = [ + "derive_builder_macro", +] + +[[package]] +name = "derive_builder_core" +version = "0.11.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1f91d4cfa921f1c05904dc3c57b4a32c38aed3340cce209f3a6fd1478babafc4" +dependencies = [ + "darling", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "derive_builder_macro" +version = "0.11.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f0314b72bed045f3a68671b3c86328386762c93f82d98c65c3cb5e5f573dd68" +dependencies = [ + "derive_builder_core", + "syn", +] + +[[package]] +name = "digest" +version = "0.10.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2fb860ca6fafa5552fb6d0e816a69c8e49f0908bf524e30a90d97c85892d506" +dependencies = [ + "block-buffer", + "crypto-common", +] + +[[package]] +name = "fnv" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" + +[[package]] +name = "generic-array" +version = "0.14.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fd48d33ec7f05fbfa152300fdad764757cbded343c1aa1cff2fbaf4134851803" +dependencies = [ + "typenum", + "version_check", +] + +[[package]] +name = "ident_case" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39" + +[[package]] +name = "itoa" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1aab8fc367588b89dcee83ab0fd66b72b50b72fa1904d7095045ace2b0c81c35" + +[[package]] +name = "libc" +version = "0.2.125" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5916d2ae698f6de9bfb891ad7a8d65c09d232dc58cc4ac433c7da3b2fd84bc2b" + +[[package]] +name = "mime" +version = "0.3.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2a60c7ce501c71e03a9c9c0d35b861413ae925bd979cc7a4e30d060069aaac8d" + +[[package]] +name = "mime_guess" +version = "2.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4192263c238a5f0d0c6bfd21f336a313a4ce1c450542449ca191bb657b4642ef" +dependencies = [ + "mime", + "unicase", +] + +[[package]] +name = "pativu-cachebust-util" +version = "0.1.0" +dependencies = [ + "cache-buster", + "serde", + "serde_json", +] + +[[package]] +name = "proc-macro2" +version = "1.0.38" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9027b48e9d4c9175fa2218adf3557f91c1137021739951d4932f5f8268ac48aa" +dependencies = [ + "unicode-xid", +] + +[[package]] +name = "quote" +version = "1.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1feb54ed693b93a84e14094943b84b7c4eae204c512b7ccb95ab0c66d278ad1" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "ryu" +version = "1.0.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "73b4b750c782965c211b42f022f59af1fbceabdd026623714f104152f1ec149f" + +[[package]] +name = "same-file" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502" +dependencies = [ + "winapi-util", +] + +[[package]] +name = "serde" +version = "1.0.137" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "61ea8d54c77f8315140a05f4c7237403bf38b72704d031543aa1d16abbf517d1" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.137" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1f26faba0c3959972377d3b2d306ee9f71faee9714294e41bb777f83f88578be" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "serde_json" +version = "1.0.81" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b7ce2b32a1aed03c558dc61a5cd328f15aff2dbc17daad8fb8af04d2100e15c" +dependencies = [ + "itoa", + "ryu", + "serde", +] + +[[package]] +name = "sha2" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "55deaec60f81eefe3cce0dc50bda92d6d8e88f2a27df7c5033b42afeb1ed2676" +dependencies = [ + "cfg-if", + "cpufeatures", + "digest", +] + +[[package]] +name = "strsim" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623" + +[[package]] +name = "syn" +version = "1.0.94" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a07e33e919ebcd69113d5be0e4d70c5707004ff45188910106854f38b960df4a" +dependencies = [ + "proc-macro2", + "quote", + "unicode-xid", +] + +[[package]] +name = "typenum" +version = "1.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dcf81ac59edc17cc8697ff311e8f5ef2d99fcbd9817b34cec66f90b6c3dfd987" + +[[package]] +name = "unicase" +version = "2.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "50f37be617794602aabbeee0be4f259dc1778fabe05e2d67ee8f79326d5cb4f6" +dependencies = [ + "version_check", +] + +[[package]] +name = "unicode-xid" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "957e51f3646910546462e67d5f7599b9e4fb8acdd304b087a6494730f9eebf04" + +[[package]] +name = "version_check" +version = "0.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f" + +[[package]] +name = "walkdir" +version = "2.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "808cf2735cd4b6866113f648b791c6adc5714537bc222d9347bb203386ffda56" +dependencies = [ + "same-file", + "winapi", + "winapi-util", +] + +[[package]] +name = "winapi" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" +dependencies = [ + "winapi-i686-pc-windows-gnu", + "winapi-x86_64-pc-windows-gnu", +] + +[[package]] +name = "winapi-i686-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" + +[[package]] +name = "winapi-util" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "70ec6ce85bb158151cae5e5c87f95a8e97d2c0c4b001223f33a334e3ce5de178" +dependencies = [ + "winapi", +] + +[[package]] +name = "winapi-x86_64-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" diff --git a/utils/cache-bust/Cargo.toml b/utils/cache-bust/Cargo.toml new file mode 100644 index 0000000..86e8ce8 --- /dev/null +++ b/utils/cache-bust/Cargo.toml @@ -0,0 +1,19 @@ +[package] +name = "pativu-cachebust-util" +version = "0.1.0" +edition = "2021" +homepage = "https://git.batsense.net/realaravinth/pativu" +repository = "https://git.batsense.net/realaravinth/pativu" +documentation = "https://git.batsense.net/realaravinth/pativu" +readme = "https://git.batsense.net/realaravinth/pativu/blob/master/README.md" +license = "AGPLv3 or later version" +authors = ["realaravinth "] + + + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +cache-buster = { version = "0.2.0", git = "https://github.com/realaravinth/cache-buster" } +serde = { version = "1", features = ["derive"] } +serde_json = "1" diff --git a/utils/cache-bust/src/main.rs b/utils/cache-bust/src/main.rs new file mode 100644 index 0000000..8ea6178 --- /dev/null +++ b/utils/cache-bust/src/main.rs @@ -0,0 +1,78 @@ +/* + * Copyright (C) 2021 Aravinth Manivannan + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +use std::fs; +use std::path::Path; +use std::collections::HashMap; + +use cache_buster::{BusterBuilder, CACHE_BUSTER_DATA_FILE, NoHashCategory}; +use serde::{Serialize, Deserialize}; + +#[derive(Deserialize, Serialize)] +struct FileMap { + map: HashMap, + base_dir: String, +} + +fn main() { + cache_bust(); + process_file_map(); +} + +fn cache_bust() { + // until APPLICATION_WASM gets added to mime crate + // PR: https://github.com/hyperium/mime/pull/138 + // let types = vec![ + // mime::IMAGE_PNG, + // mime::IMAGE_SVG, + // mime::IMAGE_JPEG, + // mime::IMAGE_GIF, + // mime::APPLICATION_JAVASCRIPT, + // mime::TEXT_CSS, + // ]; + + println!("[*] Cache busting"); + let no_hash = vec![NoHashCategory::FileExtentions(vec!["wasm"])]; + + let config = BusterBuilder::default() + .source("../../static/cache/") + .result("./../../assets") + .no_hash(no_hash) + .follow_links(true) + .build() + .unwrap(); + + config.process().unwrap(); +} + +fn process_file_map() { + let contents = fs::read_to_string(CACHE_BUSTER_DATA_FILE).unwrap(); + let files: FileMap = serde_json::from_str(&contents).unwrap(); + let mut map = HashMap::with_capacity(files.map.len()); + for (k, v) in files.map.iter() { + map.insert(k.strip_prefix("../.").unwrap().to_owned(), + v.strip_prefix("./../.").unwrap().to_owned() + ); + } + + let new_filemap = FileMap{ + map, + base_dir: files.base_dir.strip_prefix("./../.").unwrap().to_owned(), + }; + + let dest = Path::new("../../").join(CACHE_BUSTER_DATA_FILE); + fs::write(&dest, serde_json::to_string(&new_filemap).unwrap()).unwrap(); +}