From fa4aa03baa1c1980039b661f23d67505aa3334a7 Mon Sep 17 00:00:00 2001 From: Aravinth Manivannan Date: Fri, 2 Dec 2022 19:07:09 +0530 Subject: [PATCH] dump: wip archiver --- .dockerignore | 3 + .gitignore | 6 + Cargo.lock | 3692 +++++++++++++++++ Cargo.toml | 104 + Makefile | 72 + README.md | 22 + build.rs | 26 + config/default.toml | 37 + .../20220910140647_librepages_users.sql | 7 + .../20220921122103_librepages_sites.sql | 5 + package-lock.json | 385 ++ package.json | 25 + sqlx-data.json | 3 + src/api/mod.rs | 17 + src/api/v1/account/mod.rs | 144 + src/api/v1/account/test.rs | 296 ++ src/api/v1/auth.rs | 74 + src/api/v1/meta.rs | 114 + src/api/v1/mod.rs | 44 + src/api/v1/pages.rs | 100 + src/api/v1/routes.rs | 117 + src/api/v1/tests/auth.rs | 174 + src/api/v1/tests/mod.rs | 18 + src/api/v1/tests/protected.rs | 70 + src/cache_buster_data.json | 1 + src/ctx/api/mod.rs | 17 + src/ctx/api/v1/account.rs | 136 + src/ctx/api/v1/auth.rs | 104 + src/ctx/api/v1/mod.rs | 22 + src/ctx/api/v1/pages.rs | 53 + src/ctx/api/v1/tests/accounts.rs | 227 + src/ctx/api/v1/tests/auth.rs | 104 + src/ctx/api/v1/tests/mod.rs | 2 + src/ctx/mod.rs | 73 + src/db.rs | 624 +++ src/errors.rs | 268 ++ src/main.rs | 159 + src/page.rs | 93 + src/pages/auth/login.rs | 121 + src/pages/auth/mod.rs | 162 + src/pages/auth/register.rs | 109 + src/pages/auth/test.rs | 144 + src/pages/dash/home.rs | 76 + src/pages/dash/mod.rs | 35 + src/pages/dash/sites.rs | 100 + src/pages/errors.rs | 105 + src/pages/mod.rs | 209 + src/pages/routes.rs | 105 + src/serve.rs | 76 + src/settings.rs | 170 + src/static_assets/filemap.rs | 46 + src/static_assets/mod.rs | 56 + src/static_assets/static_files.rs | 94 + src/tests.rs | 274 ++ src/utils.rs | 42 + static/cache/css/main.css | 423 ++ static/cache/css/main.css.map | 1 + static/cache/css/mobile.css | 209 + static/cache/css/mobile.css.map | 1 + templates/components/base.html | 15 + templates/components/error.html | 6 + templates/components/footer.html | 37 + templates/components/nav/auth.html | 28 + templates/components/nav/base.html | 18 + templates/components/nav/pub.html | 23 + templates/components/nav/sass/main.scss | 112 + templates/components/nav/sass/mobile.scss | 141 + templates/components/sass/_fullscreen.scss | 5 + templates/components/sass/_link.scss | 4 + templates/components/sass/footer/main.scss | 79 + templates/components/sass/footer/mobile.scss | 52 + templates/defaults.scss | 70 + templates/main.scss | 5 + templates/mobile.scss | 3 + templates/pages/auth/base.html | 46 + templates/pages/auth/login.html | 44 + templates/pages/auth/register.html | 73 + templates/pages/auth/sass/form/main.scss | 29 + templates/pages/auth/sass/main.scss | 152 + templates/pages/auth/sass/mobile.scss | 36 + templates/pages/dash/index.html | 154 + templates/pages/dash/sites/add.html | 144 + utils/cache-bust/.gitignore | 2 + utils/cache-bust/Cargo.lock | 354 ++ utils/cache-bust/Cargo.toml | 19 + utils/cache-bust/src/main.rs | 78 + 86 files changed, 11725 insertions(+) create mode 100644 .dockerignore create mode 100644 .gitignore create mode 100644 Cargo.lock create mode 100644 Cargo.toml create mode 100644 Makefile create mode 100644 README.md create mode 100644 build.rs create mode 100644 config/default.toml create mode 100644 migrations/20220910140647_librepages_users.sql create mode 100644 migrations/20220921122103_librepages_sites.sql create mode 100644 package-lock.json create mode 100644 package.json create mode 100644 sqlx-data.json create mode 100644 src/api/mod.rs create mode 100644 src/api/v1/account/mod.rs create mode 100644 src/api/v1/account/test.rs create mode 100644 src/api/v1/auth.rs create mode 100644 src/api/v1/meta.rs create mode 100644 src/api/v1/mod.rs create mode 100644 src/api/v1/pages.rs create mode 100644 src/api/v1/routes.rs create mode 100644 src/api/v1/tests/auth.rs create mode 100644 src/api/v1/tests/mod.rs create mode 100644 src/api/v1/tests/protected.rs create mode 100644 src/cache_buster_data.json create mode 100644 src/ctx/api/mod.rs create mode 100644 src/ctx/api/v1/account.rs create mode 100644 src/ctx/api/v1/auth.rs create mode 100644 src/ctx/api/v1/mod.rs create mode 100644 src/ctx/api/v1/pages.rs create mode 100644 src/ctx/api/v1/tests/accounts.rs create mode 100644 src/ctx/api/v1/tests/auth.rs create mode 100644 src/ctx/api/v1/tests/mod.rs create mode 100644 src/ctx/mod.rs create mode 100644 src/db.rs create mode 100644 src/errors.rs create mode 100644 src/main.rs create mode 100644 src/page.rs create mode 100644 src/pages/auth/login.rs create mode 100644 src/pages/auth/mod.rs create mode 100644 src/pages/auth/register.rs create mode 100644 src/pages/auth/test.rs create mode 100644 src/pages/dash/home.rs create mode 100644 src/pages/dash/mod.rs create mode 100644 src/pages/dash/sites.rs create mode 100644 src/pages/errors.rs create mode 100644 src/pages/mod.rs create mode 100644 src/pages/routes.rs create mode 100644 src/serve.rs create mode 100644 src/settings.rs create mode 100644 src/static_assets/filemap.rs create mode 100644 src/static_assets/mod.rs create mode 100644 src/static_assets/static_files.rs create mode 100644 src/tests.rs create mode 100644 src/utils.rs create mode 100644 static/cache/css/main.css create mode 100644 static/cache/css/main.css.map create mode 100644 static/cache/css/mobile.css create mode 100644 static/cache/css/mobile.css.map create mode 100644 templates/components/base.html create mode 100644 templates/components/error.html create mode 100644 templates/components/footer.html create mode 100644 templates/components/nav/auth.html create mode 100644 templates/components/nav/base.html create mode 100644 templates/components/nav/pub.html create mode 100644 templates/components/nav/sass/main.scss create mode 100644 templates/components/nav/sass/mobile.scss create mode 100644 templates/components/sass/_fullscreen.scss create mode 100644 templates/components/sass/_link.scss create mode 100644 templates/components/sass/footer/main.scss create mode 100644 templates/components/sass/footer/mobile.scss create mode 100644 templates/defaults.scss create mode 100644 templates/main.scss create mode 100644 templates/mobile.scss create mode 100644 templates/pages/auth/base.html create mode 100644 templates/pages/auth/login.html create mode 100644 templates/pages/auth/register.html create mode 100644 templates/pages/auth/sass/form/main.scss create mode 100644 templates/pages/auth/sass/main.scss create mode 100644 templates/pages/auth/sass/mobile.scss create mode 100644 templates/pages/dash/index.html create mode 100644 templates/pages/dash/sites/add.html create mode 100644 utils/cache-bust/.gitignore create mode 100644 utils/cache-bust/Cargo.lock create mode 100644 utils/cache-bust/Cargo.toml create mode 100644 utils/cache-bust/src/main.rs 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(); +}