commit a4251a772a34f8ce85340cc872cb2246c5e63dc1 Author: realaravinth Date: Mon Oct 4 21:21:10 2021 +0530 init diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml new file mode 100644 index 0000000..1950079 --- /dev/null +++ b/.github/FUNDING.yml @@ -0,0 +1,12 @@ +# These are supported funding model platforms + +# github: # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2] +# patreon: # Replace with a single Patreon username +open_collective: # Replace with a single Open Collective username +# ko_fi: # Replace with a single Ko-fi username +# tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel +# community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry +liberapay: mcaptcha +issuehunt: # Replace with a single IssueHunt username +# otechie: # Replace with a single Otechie username +custom: ['https://mcaptcha.org/donate'] diff --git a/.github/workflows/clippy-fmt.yml b/.github/workflows/clippy-fmt.yml new file mode 100644 index 0000000..325e539 --- /dev/null +++ b/.github/workflows/clippy-fmt.yml @@ -0,0 +1,43 @@ +name: Lint + +on: + pull_request: + types: [opened, synchronize, reopened] + push: + branches: + - master + +jobs: + fmt: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + + - name: Install Rust + uses: actions-rs/toolchain@v1 + with: + toolchain: stable + components: rustfmt + - name: Check with rustfmt + uses: actions-rs/cargo@v1 + with: + command: fmt + args: --all -- --check + + clippy: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + + - name: Install Rust + uses: actions-rs/toolchain@v1 + with: + toolchain: stable + components: clippy + override: true + + - name: Check with Clippy + uses: actions-rs/clippy-check@v1 + with: + token: ${{ secrets.GITHUB_TOKEN }} + args: --workspace --tests --all-features diff --git a/.github/workflows/coverage.yml b/.github/workflows/coverage.yml new file mode 100644 index 0000000..70accc1 --- /dev/null +++ b/.github/workflows/coverage.yml @@ -0,0 +1,79 @@ +name: Coverage + +on: + pull_request: + types: [opened, synchronize, reopened] + push: + branches: + - master + +jobs: + build_and_test: + strategy: + fail-fast: false + matrix: + version: + # - stable + - 1.51.0 + + name: ${{ matrix.version }} - x86_64-unknown-linux-gnu + runs-on: ubuntu-latest + + services: + postgres: + image: postgres + env: + POSTGRES_PASSWORD: password + POSTGRES_USER: postgres + POSTGRES_DB: postgres + options: >- + --health-cmd pg_isready + --health-interval 10s + --health-timeout 5s + --health-retries 5 + ports: + - 5432:5432 + + steps: + - uses: actions/checkout@v2 + - name: ⚡ Cache + uses: actions/cache@v2 + with: + path: | + ~/.cargo/registry + ~/.cargo/git + target + key: ${{ runner.os }}-cargo-${{ hashFiles('**/Cargo.lock') }} + + - name: Install ${{ matrix.version }} + uses: actions-rs/toolchain@v1 + with: + toolchain: ${{ matrix.version }}-x86_64-unknown-linux-gnu + profile: minimal + override: true + + - name: build sass + run: make frontend + + - name: Run migrations + run: make migrate + env: + DATABASE_URL: postgres://postgres:password@localhost:5432/postgres + + - name: Generate coverage file + if: matrix.version == '1.51.0' && (github.ref == 'refs/heads/master' || github.event_name == 'pull_request') + uses: actions-rs/tarpaulin@v0.1 + with: + version: '0.15.0' + args: '-t 1200' + env: + DATABASE_URL: postgres://postgres:password@localhost:5432/postgres + # GIT_HASH is dummy value. I guess build.rs is skipped in tarpaulin + # execution so this value is required for preventing meta tests from + # panicking + GIT_HASH: 8e77345f1597e40c2e266cb4e6dee74888918a61 + COMPILED_DATE: "2021-07-21" + + - name: Upload to Codecov + if: matrix.version == '1.51.0' && (github.ref == 'refs/heads/master' || github.event_name == 'pull_request') + uses: codecov/codecov-action@v1 diff --git a/.github/workflows/linux.yml b/.github/workflows/linux.yml new file mode 100644 index 0000000..4d2d955 --- /dev/null +++ b/.github/workflows/linux.yml @@ -0,0 +1,90 @@ +name: Build + +on: + pull_request: + types: [opened, synchronize, reopened] + push: + branches: + - master + +jobs: + build_and_test: + strategy: + fail-fast: false + matrix: + version: + #- 1.51.0 + - stable + # - nightly + + name: ${{ matrix.version }} - x86_64-unknown-linux-gnu + runs-on: ubuntu-latest + + services: + postgres: + image: postgres + env: + POSTGRES_PASSWORD: password + POSTGRES_USER: postgres + POSTGRES_DB: postgres + options: >- + --health-cmd pg_isready + --health-interval 10s + --health-timeout 5s + --health-retries 5 + ports: + - 5432:5432 + + steps: + - uses: actions/checkout@v2 + - name: ⚡ Cache + uses: actions/cache@v2 + with: + path: | + ~/.cargo/registry + ~/.cargo/git + target + key: ${{ runner.os }}-cargo-${{ hashFiles('**/Cargo.lock') }} + + + - name: Install ${{ matrix.version }} + uses: actions-rs/toolchain@v1 + with: + toolchain: ${{ matrix.version }}-x86_64-unknown-linux-gnu + profile: minimal + override: true + + - name: build sass + run: make frontend + + - name: Run migrations + run: make migrate + env: + DATABASE_URL: postgres://postgres:password@localhost:5432/postgres + + - name: build + run: make + env: + DATABASE_URL: postgres://postgres:password@localhost:5432/postgres + + - name: run tests + run: make test + env: + DATABASE_URL: postgres://postgres:password@localhost:5432/postgres + + - name: generate documentation + if: matrix.version == 'stable' && (github.repository == 'mcapthca/survey') + run: make doc + env: + DATABASE_URL: postgres://postgres:password@localhost:5432/postgres + GIT_HASH: 8e77345f1597e40c2e266cb4e6dee74888918a61 # dummy value + OPEN_API_DOCS: 8e77345f1597e40c2e266cb4e6dee74888918a61 + COMPILED_DATE: "2021-07-21" + + - name: Deploy to GitHub Pages + if: matrix.version == 'stable' && (github.repository == 'mcapthca/survey') + uses: JamesIves/github-pages-deploy-action@3.7.1 + with: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + BRANCH: gh-pages + FOLDER: target/doc diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..4aa3436 --- /dev/null +++ b/.gitignore @@ -0,0 +1,15 @@ +/target +tarpaulin-report.html +.env +cobertura.xml +prod/ +node_modules/ +/static-assets/bundle +static/cache/bundle +./templates/**/*.js +/static-assets/bundle/* +src/cache_buster_data.json +coverage +dist +assets +scripts/creds.py diff --git a/Cargo.lock b/Cargo.lock new file mode 100644 index 0000000..a0a2f74 --- /dev/null +++ b/Cargo.lock @@ -0,0 +1,2914 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 3 + +[[package]] +name = "actix" +version = "0.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3720d0064a0ce5c0de7bd93bdb0a6caebab2a9b5668746145d7b3b0c5da02914" +dependencies = [ + "actix-rt", + "actix_derive", + "bitflags", + "bytes", + "crossbeam-channel", + "futures-core", + "futures-sink", + "futures-task", + "futures-util", + "log", + "once_cell", + "parking_lot", + "pin-project-lite", + "smallvec", + "tokio", + "tokio-util", +] + +[[package]] +name = "actix-codec" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d5dbeb2d9e51344cb83ca7cc170f1217f9fe25bfc50160e6e200b5c31c1019a" +dependencies = [ + "bitflags", + "bytes", + "futures-core", + "futures-sink", + "log", + "pin-project-lite", + "tokio", + "tokio-util", +] + +[[package]] +name = "actix-cors" +version = "0.6.0-beta.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "01552b8facccd5d7a4cc5d8e2b07d306160c97a4968181c2db965533389c8725" +dependencies = [ + "actix-service", + "actix-web", + "derive_more", + "futures-util", + "log", + "once_cell", + "smallvec", +] + +[[package]] +name = "actix-http" +version = "3.0.0-beta.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dd38a862fa7fead2b47ee55e550982aba583ebc7365ccf0155b49934ad6f16f9" +dependencies = [ + "actix-codec", + "actix-rt", + "actix-service", + "actix-tls", + "actix-utils", + "ahash", + "base64", + "bitflags", + "brotli2", + "bytes", + "bytestring", + "derive_more", + "encoding_rs", + "flate2", + "futures-core", + "futures-util", + "h2", + "http", + "httparse", + "itoa", + "language-tags", + "local-channel", + "log", + "mime", + "once_cell", + "percent-encoding", + "pin-project", + "pin-project-lite", + "rand", + "regex", + "serde 1.0.130", + "sha-1", + "smallvec", + "time", + "tokio", + "zstd", +] + +[[package]] +name = "actix-identity" +version = "0.4.0-beta.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "50b26f099c3b3bd459a547abf391c3cbecdc077df757f56856e31e8cd5757f28" +dependencies = [ + "actix-service", + "actix-web", + "futures-util", + "serde 1.0.130", + "serde_json", + "time", +] + +[[package]] +name = "actix-macros" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2f86cd6857c135e6e9fe57b1619a88d1f94a7df34c00e11fe13e64fd3438837" +dependencies = [ + "quote", + "syn", +] + +[[package]] +name = "actix-router" +version = "0.5.0-beta.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "36b95ce0d76d1aa2f98b681702807475ade0f99bd4552546a6843a966d42ea3d" +dependencies = [ + "bytestring", + "firestorm", + "http", + "log", + "regex", + "serde 1.0.130", +] + +[[package]] +name = "actix-rt" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bc7d7cd957c9ed92288a7c3c96af81fa5291f65247a76a34dac7b6af74e52ba0" +dependencies = [ + "actix-macros", + "futures-core", + "tokio", +] + +[[package]] +name = "actix-server" +version = "2.0.0-beta.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26369215fcc3b0176018b3b68756a8bcc275bb000e6212e454944913a1f9bf87" +dependencies = [ + "actix-rt", + "actix-service", + "actix-utils", + "futures-core", + "log", + "mio", + "num_cpus", + "slab", + "tokio", +] + +[[package]] +name = "actix-service" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77f5f9d66a8730d0fae62c26f3424f5751e5518086628a40b7ab6fca4a705034" +dependencies = [ + "futures-core", + "paste", + "pin-project-lite", +] + +[[package]] +name = "actix-tls" +version = "3.0.0-beta.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "65b7bb60840962ef0332f7ea01a57d73a24d2cb663708511ff800250bbfef569" +dependencies = [ + "actix-codec", + "actix-rt", + "actix-service", + "actix-utils", + "derive_more", + "futures-core", + "http", + "log", + "tokio-util", +] + +[[package]] +name = "actix-utils" +version = "3.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e491cbaac2e7fc788dfff99ff48ef317e23b3cf63dbaf7aaab6418f40f92aa94" +dependencies = [ + "local-waker", + "pin-project-lite", +] + +[[package]] +name = "actix-web" +version = "4.0.0-beta.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d34aa2b23ec9c7c9a799b3cf9258f67c91b18ac3f0f5f484e175c7ac46739bb5" +dependencies = [ + "actix-codec", + "actix-http", + "actix-macros", + "actix-router", + "actix-rt", + "actix-server", + "actix-service", + "actix-utils", + "actix-web-codegen 0.5.0-beta.4 (registry+https://github.com/rust-lang/crates.io-index)", + "ahash", + "bytes", + "cfg-if", + "cookie", + "derive_more", + "either", + "encoding_rs", + "futures-core", + "futures-util", + "itoa", + "language-tags", + "log", + "mime", + "once_cell", + "paste", + "pin-project", + "regex", + "serde 1.0.130", + "serde_json", + "serde_urlencoded", + "smallvec", + "socket2", + "time", + "url", +] + +[[package]] +name = "actix-web-codegen" +version = "0.5.0-beta.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4a11fd6f322120a74b23327e778ef0a4950b1f44a2b76468a69316a150f5c6dd" +dependencies = [ + "actix-router", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "actix-web-codegen" +version = "0.5.0-beta.4" +source = "git+https://github.com/realaravinth/actix-web#292317ff1eae7db38bd0aa9900ede5fadc9216d1" +dependencies = [ + "actix-router", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "actix_derive" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6d44b8fee1ced9671ba043476deddef739dd0959bf77030b26b738cc591737a7" +dependencies = [ + "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.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7fc95d1bdb8e6666b2b217308eeeb09f2d6728d104be3e31916cc74d15420331" +dependencies = [ + "generic-array", +] + +[[package]] +name = "aes" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "884391ef1066acaa41e766ba8f596341b96e93ce34f9a43e7d24bf0a0eaf0561" +dependencies = [ + "aes-soft", + "aesni", + "cipher", +] + +[[package]] +name = "aes-gcm" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5278b5fabbb9bd46e24aa69b2fdea62c99088e0a950a9be40e3e0101298f88da" +dependencies = [ + "aead", + "aes", + "cipher", + "ctr", + "ghash", + "subtle", +] + +[[package]] +name = "aes-soft" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "be14c7498ea50828a38d0e24a765ed2effe92a705885b57d029cd67d45744072" +dependencies = [ + "cipher", + "opaque-debug", +] + +[[package]] +name = "aesni" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ea2e11f5e94c2f7d386164cc2aa1f97823fed6f259e486940a71c174dd01b0ce" +dependencies = [ + "cipher", + "opaque-debug", +] + +[[package]] +name = "ahash" +version = "0.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "43bb833f0bf979d8475d38fbf09ed3b8a55e1885fe93ad3f93239fc6a4f17b98" +dependencies = [ + "getrandom", + "once_cell", + "version_check", +] + +[[package]] +name = "aho-corasick" +version = "0.7.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e37cfd5e7657ada45f742d6e99ca5788580b5c529dc78faf11ece6dc702656f" +dependencies = [ + "memchr", +] + +[[package]] +name = "arrayvec" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "23b62fc65de8e4e7f52534fb52b0f3ed04746ae267519eef2a83941e8085068b" + +[[package]] +name = "async-trait" +version = "0.1.51" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "44318e776df68115a881de9a8fd1b9e53368d7a4a5ce4cc48517da3393233a5e" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "atoi" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "616896e05fc0e2649463a93a15183c6a16bf03413a7af88ef1285ddedfa9cda5" +dependencies = [ + "num-traits 0.2.14", +] + +[[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.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cdb031dd78e28731d87d56cc8ffef4a8f36ca26c38fe2de700543e627f8a464a" + +[[package]] +name = "base-x" +version = "0.2.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4521f3e3d031370679b3b140beb36dfe4801b09ac77e30c61941f97df3ef28b" + +[[package]] +name = "base64" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "904dfeac50f3cdaba28fc6f57fdcddb75f49ed61346676a78c4ffe55877802fd" + +[[package]] +name = "bincode" +version = "1.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b1f45e9417d87227c7a56d22e471c6206462cba514c7590c09aff4cf6d1ddcad" +dependencies = [ + "serde 1.0.130", +] + +[[package]] +name = "bitflags" +version = "1.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" + +[[package]] +name = "block-buffer" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4152116fd6e9dadb291ae18fc1ec3575ed6d84c29642d97890f4b4a3417297e4" +dependencies = [ + "generic-array", +] + +[[package]] +name = "brotli-sys" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4445dea95f4c2b41cde57cc9fee236ae4dbae88d8fcbdb4750fc1bb5d86aaecd" +dependencies = [ + "cc", + "libc", +] + +[[package]] +name = "brotli2" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0cb036c3eade309815c15ddbacec5b22c4d1f3983a774ab2eac2e3e9ea85568e" +dependencies = [ + "brotli-sys", + "libc", +] + +[[package]] +name = "bumpalo" +version = "3.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9df67f7bf9ef8498769f994239c45613ef0c5899415fb58e9add412d2c1a538" + +[[package]] +name = "byteorder" +version = "1.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "14c189c53d098945499cdfa7ecc63567cf3886b3332b312a5b4585d8d3a6a610" + +[[package]] +name = "bytes" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c4872d67bab6358e59559027aa3b9157c53d9358c51423c17554809a8858e0f8" + +[[package]] +name = "bytestring" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "90706ba19e97b90786e19dc0d5e2abd80008d99d4c0c5d1ad0b5e72cec7c494d" +dependencies = [ + "bytes", +] + +[[package]] +name = "cache-buster" +version = "0.2.0" +source = "git+https://github.com/realaravinth/cache-buster#e01e7bcc1d0c79b6f51648e42e8c027f894dc514" +dependencies = [ + "data-encoding", + "derive_builder", + "mime", + "mime_guess", + "serde 1.0.130", + "serde_json", + "sha2", + "walkdir", +] + +[[package]] +name = "cc" +version = "1.0.70" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d26a6ce4b6a484fa3edb70f7efa6fc430fd2b87285fe8b84304fd0936faa0dc0" +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.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "12f8e7987cbd042a63249497f41aed09f8e65add917ea6566effbc56578d6801" +dependencies = [ + "generic-array", +] + +[[package]] +name = "combine" +version = "4.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a909e4d93292cd8e9c42e189f61681eff9d67b6541f96b8a1a737f23737bd001" +dependencies = [ + "bytes", + "futures-core", + "memchr", + "pin-project-lite", + "tokio", + "tokio-util", +] + +[[package]] +name = "config" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b1b9d958c2b1368a663f05538fc1b5975adce1e19f435acceae987aceeeb369" +dependencies = [ + "lazy_static", + "nom 5.1.2", + "rust-ini", + "serde 1.0.130", + "serde-hjson", + "serde_json", + "toml", + "yaml-rust", +] + +[[package]] +name = "const_fn" +version = "0.4.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f92cfa0fd5690b3cf8c1ef2cabbd9b7ef22fa53cf5e1f92b05103f6d5d1cf6e7" + +[[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.15.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d5f1c7727e460397e56abc4bddc1d49e07a1ad78fc98eb2e1c8f032a58a2f80d" +dependencies = [ + "aes-gcm", + "base64", + "hkdf", + "hmac", + "percent-encoding", + "rand", + "sha2", + "subtle", + "time", + "version_check", +] + +[[package]] +name = "cpufeatures" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "95059428f66df56b63431fdb4e1947ed2190586af5c5a8a8b71122bdf5a7f469" +dependencies = [ + "libc", +] + +[[package]] +name = "cpuid-bool" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dcb25d077389e53838a8158c8e99174c5a9d902dee4904320db714f3c653ffba" + +[[package]] +name = "crc" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "10c2722795460108a7872e1cd933a85d6ec38abc4baecad51028f702da28889f" +dependencies = [ + "crc-catalog", +] + +[[package]] +name = "crc-catalog" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ccaeedb56da03b09f598226e25e80088cb4cd25f316e6e4df7d695f0feeb1403" + +[[package]] +name = "crc16" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "338089f42c427b86394a5ee60ff321da23a5c89c9d89514c829687b26359fcff" + +[[package]] +name = "crc32fast" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "81156fece84ab6a9f2afdb109ce3ae577e42b1228441eded99bd77f627953b1a" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "crossbeam-channel" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06ed27e177f16d65f0f0c22a213e17c696ace5dd64b14258b52f9417ccb52db4" +dependencies = [ + "cfg-if", + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-queue" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b10ddc024425c88c2ad148c1b0fd53f4c6d38db9697c9f1588381212fa657c9" +dependencies = [ + "cfg-if", + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-utils" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d82cfc11ce7f2c3faef78d8a684447b40d503d9681acebed6cb728d45940c4db" +dependencies = [ + "cfg-if", + "lazy_static", +] + +[[package]] +name = "crypto-mac" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bff07008ec701e8028e2ceb8f83f0e4274ee62bd2dbdc4fefff2e9a91824081a" +dependencies = [ + "generic-array", + "subtle", +] + +[[package]] +name = "ctr" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fb4a30d54f7443bf3d6191dcd486aca19e67cb3c49fa7a06a319966346707e7f" +dependencies = [ + "cipher", +] + +[[package]] +name = "darling" +version = "0.12.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5f2c43f534ea4b0b049015d00269734195e6d3f0f6635cb692251aca6f9f8b3c" +dependencies = [ + "darling_core", + "darling_macro", +] + +[[package]] +name = "darling_core" +version = "0.12.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e91455b86830a1c21799d94524df0845183fa55bafd9aa137b01c7d1065fa36" +dependencies = [ + "fnv", + "ident_case", + "proc-macro2", + "quote", + "strsim", + "syn", +] + +[[package]] +name = "darling_macro" +version = "0.12.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29b5acf0dea37a7f66f7b25d2c5e93fd46f8f6968b1a5d7a3e02e97768afc95a" +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.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d13202debe11181040ae9063d739fa32cfcaaebe2275fe387703460ae2365b30" +dependencies = [ + "derive_builder_macro", +] + +[[package]] +name = "derive_builder_core" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "66e616858f6187ed828df7c64a6d71720d83767a7f19740b2d1b6fe6327b36e5" +dependencies = [ + "darling", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "derive_builder_macro" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "58a94ace95092c5acb1e97a7e846b310cfbd499652f72297da7493f618a98d73" +dependencies = [ + "derive_builder_core", + "syn", +] + +[[package]] +name = "derive_more" +version = "0.99.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "40eebddd2156ce1bb37b20bbe5151340a31828b1f2d22ba4141f3531710e38df" +dependencies = [ + "convert_case", + "proc-macro2", + "quote", + "rustc_version 0.3.3", + "syn", +] + +[[package]] +name = "digest" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3dd60d1080a57a05ab032377049e0591415d2b31afd7028356dbf3cc6dcb066" +dependencies = [ + "generic-array", +] + +[[package]] +name = "dirs" +version = "3.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "30baa043103c9d0c2a57cf537cc2f35623889dc0d405e6c3cccfadbc81c71309" +dependencies = [ + "dirs-sys", +] + +[[package]] +name = "dirs-sys" +version = "0.3.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "03d86534ed367a67548dc68113a0f5db55432fdfbb6e6f9d77704397d95d5780" +dependencies = [ + "libc", + "redox_users", + "winapi", +] + +[[package]] +name = "discard" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "212d0f5754cb6769937f4501cc0e67f4f4483c8d2c3e1e922ee9edbe4ab4c7c0" + +[[package]] +name = "dotenv" +version = "0.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77c90badedccf4105eca100756a0b1289e191f6fcbdadd3cee1d2f614f97da8f" + +[[package]] +name = "dtoa" +version = "0.4.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56899898ce76aaf4a0f24d914c97ea6ed976d42fec6ad33fcbb0a1103e07b2b0" + +[[package]] +name = "either" +version = "1.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e78d4f1cc4ae33bbfc157ed5d5a5ef3bc29227303d595861deb238fcec4e9457" +dependencies = [ + "serde 1.0.130", +] + +[[package]] +name = "encoding_rs" +version = "0.8.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "80df024fbc5ac80f87dfef0d9f5209a252f2a497f7f42944cff24d8253cac065" +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 = "filetime" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "975ccf83d8d9d0d84682850a38c8169027be83368805971cc4f238c2b245bc98" +dependencies = [ + "cfg-if", + "libc", + "redox_syscall", + "winapi", +] + +[[package]] +name = "firestorm" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "31586bda1b136406162e381a3185a506cdfc1631708dd40cba2f6628d8634499" + +[[package]] +name = "flate2" +version = "1.0.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e6988e897c1c9c485f43b47a529cef42fde0547f9d8d41a7062518f1d8fc53f" +dependencies = [ + "cfg-if", + "crc32fast", + "libc", + "miniz_oxide", +] + +[[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.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5fc25a87fa4fd2094bffb06925852034d90a17f0d1e05197d4956d3555752191" +dependencies = [ + "matches", + "percent-encoding", +] + +[[package]] +name = "futures" +version = "0.3.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a12aa0eb539080d55c3f2d45a67c3b58b6b0773c1a3ca2dfec66d58c97fd66ca" +dependencies = [ + "futures-channel", + "futures-core", + "futures-executor", + "futures-io", + "futures-sink", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-channel" +version = "0.3.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5da6ba8c3bb3c165d3c7319fc1cc8304facf1fb8db99c5de877183c08a273888" +dependencies = [ + "futures-core", + "futures-sink", +] + +[[package]] +name = "futures-core" +version = "0.3.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "88d1c26957f23603395cd326b0ffe64124b818f4449552f960d815cfba83a53d" + +[[package]] +name = "futures-executor" +version = "0.3.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "45025be030969d763025784f7f355043dc6bc74093e4ecc5000ca4dc50d8745c" +dependencies = [ + "futures-core", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-intrusive" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "62007592ac46aa7c2b6416f7deb9a8a8f63a01e0f1d6e1787d5630170db2b63e" +dependencies = [ + "futures-core", + "lock_api", + "parking_lot", +] + +[[package]] +name = "futures-io" +version = "0.3.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "522de2a0fe3e380f1bc577ba0474108faf3f6b18321dbf60b3b9c39a75073377" + +[[package]] +name = "futures-macro" +version = "0.3.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "18e4a4b95cea4b4ccbcf1c5675ca7c4ee4e9e75eb79944d07defde18068f79bb" +dependencies = [ + "autocfg", + "proc-macro-hack", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "futures-sink" +version = "0.3.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "36ea153c13024fe480590b3e3d4cad89a0cfacecc24577b68f86c6ced9c2bc11" + +[[package]] +name = "futures-task" +version = "0.3.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d3d00f4eddb73e498a54394f228cd55853bdf059259e8e7bc6e69d408892e99" + +[[package]] +name = "futures-util" +version = "0.3.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "36568465210a3a6ee45e1f165136d68671471a501e632e9a98d96872222b5481" +dependencies = [ + "autocfg", + "futures-channel", + "futures-core", + "futures-io", + "futures-macro", + "futures-sink", + "futures-task", + "memchr", + "pin-project-lite", + "pin-utils", + "proc-macro-hack", + "proc-macro-nested", + "slab", +] + +[[package]] +name = "generic-array" +version = "0.14.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "501466ecc8a30d1d3b7fc9229b122b2ce8ed6e9d9223f1138d4babb253e51817" +dependencies = [ + "typenum", + "version_check", +] + +[[package]] +name = "getrandom" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7fcd999463524c52659517fe2cea98493cfe485d10565e7b0fb07dbba7ad2753" +dependencies = [ + "cfg-if", + "libc", + "wasi", +] + +[[package]] +name = "ghash" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97304e4cd182c3846f7575ced3890c53012ce534ad9114046b0a9e00bb30a375" +dependencies = [ + "opaque-debug", + "polyval", +] + +[[package]] +name = "h2" +version = "0.3.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c06815895acec637cd6ed6e9662c935b866d20a106f8361892893a7d9234964" +dependencies = [ + "bytes", + "fnv", + "futures-core", + "futures-sink", + "futures-util", + "http", + "indexmap", + "slab", + "tokio", + "tokio-util", + "tracing", +] + +[[package]] +name = "hashbrown" +version = "0.11.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ab5ef0d4909ef3724cc8cce6ccc8572c5c817592e9285f5464f8e86f8bd3726e" +dependencies = [ + "ahash", +] + +[[package]] +name = "hashlink" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7249a3129cbc1ffccd74857f81464a323a152173cdb134e0fd81bc803b29facf" +dependencies = [ + "hashbrown", +] + +[[package]] +name = "heck" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6d621efb26863f0e9924c6ac577e8275e5e6b77455db64ffa6c65c904e9e132c" +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.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "51ab2f639c231793c5f6114bdb9bbe50a7dbbfcd7c7c6bd8475dec2d991e964f" +dependencies = [ + "digest", + "hmac", +] + +[[package]] +name = "hmac" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c1441c6b1e930e2817404b5046f1f989899143a12bf92de603b69f4e0aee1e15" +dependencies = [ + "crypto-mac", + "digest", +] + +[[package]] +name = "home" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2456aef2e6b6a9784192ae780c0f15bc57df0e918585282325e8c8ac27737654" +dependencies = [ + "winapi", +] + +[[package]] +name = "http" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1323096b05d41827dadeaee54c9981958c0f94e670bc94ed80037d1a7b8b186b" +dependencies = [ + "bytes", + "fnv", + "itoa", +] + +[[package]] +name = "httparse" +version = "1.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "acd94fdbe1d4ff688b67b04eee2e17bd50995534a61539e45adfefb45e5e5503" + +[[package]] +name = "humantime" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df004cfca50ef23c36850aaaa59ad52cc70d0e90243c3c7737a4dd32dc7a3c4f" +dependencies = [ + "quick-error", +] + +[[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 = "if_chain" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb56e1aa765b4b4f3aadfab769793b7087bb03a4ea4920644a6d238e2df5b9ed" + +[[package]] +name = "indexmap" +version = "1.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bc633605454125dec4b66843673f01c7df2b89479b32e0ed634e43a91cff62a5" +dependencies = [ + "autocfg", + "hashbrown", +] + +[[package]] +name = "instant" +version = "0.1.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "716d3d89f35ac6a34fd0eed635395f4c3b76fa889338a4632e5231a8684216bd" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "itertools" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "69ddb889f9d0d08a67338271fa9b62996bc788c7796a5c18cf057420aaed5eaf" +dependencies = [ + "either", +] + +[[package]] +name = "itoa" +version = "0.4.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b71991ff56294aa922b450139ee08b3bfc70982c6b2c7562771375cf73542dd4" + +[[package]] +name = "itoap" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9028f49264629065d057f340a86acb84867925865f73bbf8d47b4d149a7e88b8" + +[[package]] +name = "jobserver" +version = "0.1.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "af25a77299a7f711a01975c35a6a424eb6862092cc2d6c72c4ed6cbc56dfc1fa" +dependencies = [ + "libc", +] + +[[package]] +name = "js-sys" +version = "0.3.55" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7cc9ffccd38c451a86bf13657df244e9c3f37493cce8e5e21e940963777acc84" +dependencies = [ + "wasm-bindgen", +] + +[[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 = "lexical-core" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6607c62aa161d23d17a9072cc5da0be67cdfc89d3afb1e8d9c842bebc2525ffe" +dependencies = [ + "arrayvec", + "bitflags", + "cfg-if", + "ryu", + "static_assertions", +] + +[[package]] +name = "libc" +version = "0.2.103" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dd8f7255a17a627354f321ef0055d63b898c6fb27eff628af4d1b66b7331edf6" + +[[package]] +name = "libmcaptcha" +version = "0.1.4" +source = "git+https://github.com/mCaptcha/libmcaptcha?branch=master#48220d78e706745a5208850c832ee8ba7e704601" +dependencies = [ + "actix", + "derive_builder", + "derive_more", + "log", + "pow_sha256", + "pretty_env_logger", + "rand", + "redis", + "serde 1.0.130", + "serde_json", + "tokio", +] + +[[package]] +name = "linked-hash-map" +version = "0.5.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7fb9b38af92608140b86b693604b9ffcc5824240a484d1ecd4795bacb2fe88f3" + +[[package]] +name = "local-channel" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6246c68cf195087205a0512559c97e15eaf95198bf0e206d662092cdcb03fe9f" +dependencies = [ + "futures-core", + "futures-sink", + "futures-util", + "local-waker", +] + +[[package]] +name = "local-waker" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "84f9a2d3e27ce99ce2c3aad0b09b1a7b916293ea9b2bf624c13fe646fadd8da4" + +[[package]] +name = "lock_api" +version = "0.4.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "712a4d093c9976e24e7dbca41db895dabcbac38eb5f4045393d17a95bdfb1109" +dependencies = [ + "scopeguard", +] + +[[package]] +name = "log" +version = "0.4.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "51b9bbe6c47d51fc3e1a9b945965946b4c44142ab8792c50835a980d362c2710" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "matches" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a3e378b66a060d48947b590737b30a1be76706c8dd7b8ba0f2fe3989c68a853f" + +[[package]] +name = "md-5" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7b5a279bb9607f9f53c22d496eade00d138d1bdcccd07d74650387cf94942a15" +dependencies = [ + "block-buffer", + "digest", + "opaque-debug", +] + +[[package]] +name = "memchr" +version = "2.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "308cc39be01b73d0d18f82a0e7b2a3df85245f84af96fdddc5d202d27e47b86a" + +[[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.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2684d4c2e97d99848d30b324b00c8fcc7e5c897b7cbb5819b09e7c90e8baf212" +dependencies = [ + "mime", + "unicase", +] + +[[package]] +name = "minimal-lexical" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c835948974f68e0bd58636fc6c5b1fbff7b297e3046f11b3b3c18bbac012c6d" + +[[package]] +name = "miniz_oxide" +version = "0.4.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a92518e98c078586bc6c934028adcca4c92a53d6a958196de835170a01d84e4b" +dependencies = [ + "adler", + "autocfg", +] + +[[package]] +name = "mio" +version = "0.7.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8c2bdb6314ec10835cd3293dd268473a835c02b7b352e788be788b3c6ca6bb16" +dependencies = [ + "libc", + "log", + "miow", + "ntapi", + "winapi", +] + +[[package]] +name = "miow" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9f1c5b025cda876f66ef43a113f91ebc9f4ccef34843000e0adf6ebbab84e21" +dependencies = [ + "winapi", +] + +[[package]] +name = "nom" +version = "5.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ffb4262d26ed83a1c0a33a38fe2bb15797329c85770da05e6b828ddb782627af" +dependencies = [ + "lexical-core", + "memchr", + "version_check", +] + +[[package]] +name = "nom" +version = "7.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7ffd9d26838a953b4af82cbeb9f1592c6798916983959be223a7124e992742c1" +dependencies = [ + "memchr", + "minimal-lexical", + "version_check", +] + +[[package]] +name = "ntapi" +version = "0.3.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f6bb902e437b6d86e03cce10a7e2af662292c5dfef23b65899ea3ac9354ad44" +dependencies = [ + "winapi", +] + +[[package]] +name = "num" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "43db66d1170d347f9a065114077f7dccb00c1b9478c89384490a3425279a4606" +dependencies = [ + "num-bigint", + "num-complex", + "num-integer", + "num-iter", + "num-rational", + "num-traits 0.2.14", +] + +[[package]] +name = "num-bigint" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "74e768dff5fb39a41b3bcd30bb25cf989706c90d028d1ad71971987aa309d535" +dependencies = [ + "autocfg", + "num-integer", + "num-traits 0.2.14", + "serde 1.0.130", +] + +[[package]] +name = "num-complex" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26873667bbbb7c5182d4a37c1add32cdf09f841af72da53318fdb81543c15085" +dependencies = [ + "num-traits 0.2.14", + "serde 1.0.130", +] + +[[package]] +name = "num-integer" +version = "0.1.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d2cc698a63b549a70bc047073d2949cce27cd1c7b0a4a862d08a8031bc2801db" +dependencies = [ + "autocfg", + "num-traits 0.2.14", +] + +[[package]] +name = "num-iter" +version = "0.1.42" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b2021c8337a54d21aca0d59a92577a029af9431cb59b909b03252b9c164fad59" +dependencies = [ + "autocfg", + "num-integer", + "num-traits 0.2.14", +] + +[[package]] +name = "num-rational" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d41702bd167c2df5520b384281bc111a4b5efcf7fbc4c9c222c815b07e0a6a6a" +dependencies = [ + "autocfg", + "num-integer", + "num-traits 0.2.14", + "serde 1.0.130", +] + +[[package]] +name = "num-traits" +version = "0.1.43" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92e5113e9fd4cc14ded8e499429f396a20f98c772a47cc8622a736e1ec843c31" +dependencies = [ + "num-traits 0.2.14", +] + +[[package]] +name = "num-traits" +version = "0.2.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a64b1ec5cda2586e284722486d802acf1f7dbdc623e2bfc57e65ca1cd099290" +dependencies = [ + "autocfg", +] + +[[package]] +name = "num_cpus" +version = "1.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05499f3756671c15885fee9034446956fff3f243d6077b91e5767df161f766b3" +dependencies = [ + "hermit-abi", + "libc", +] + +[[package]] +name = "once_cell" +version = "1.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "692fcb63b64b1758029e0a96ee63e049ce8c5948587f2f7208df04625e5f6b56" + +[[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.36" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8d9facdb76fec0b73c406f125d44d86fdad818d66fef0531eec9233ca425ff4a" +dependencies = [ + "bitflags", + "cfg-if", + "foreign-types", + "libc", + "once_cell", + "openssl-sys", +] + +[[package]] +name = "openssl-src" +version = "111.16.0+1.1.1l" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7ab2173f69416cf3ec12debb5823d244127d23a9b127d5a5189aa97c5fa2859f" +dependencies = [ + "cc", +] + +[[package]] +name = "openssl-sys" +version = "0.9.67" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "69df2d8dfc6ce3aaf44b40dec6f487d5a886516cf6879c49e98e0710f310a058" +dependencies = [ + "autocfg", + "cc", + "libc", + "openssl-src", + "pkg-config", + "vcpkg", +] + +[[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", +] + +[[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 = "paste" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "acbf547ad0c65e31259204bd90935776d1c693cec2f4ff7abb7a1bbbd40dfe58" + +[[package]] +name = "percent-encoding" +version = "2.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d4fd5641d01c8f18a23da7b6fe29298ff4b55afcccdf78973b24cf3175fee32e" + +[[package]] +name = "pest" +version = "2.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "10f4872ae94d7b90ae48754df22fd42ad52ce740b8f370b03da4835417403e53" +dependencies = [ + "ucd-trie", +] + +[[package]] +name = "pin-project" +version = "1.0.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "576bc800220cc65dac09e99e97b08b358cfab6e17078de8dc5fee223bd2d0c08" +dependencies = [ + "pin-project-internal", +] + +[[package]] +name = "pin-project-internal" +version = "1.0.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e8fe8163d14ce7f0cdac2e040116f22eac817edabff0be91e8aff7e9accf389" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "pin-project-lite" +version = "0.2.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8d31d11c69a6b52a174b42bdc0c30e5e11670f90788b2c471c31c1d17d449443" + +[[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.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7c9b1041b4387893b91ee6746cddfc28516aff326a3519fb2adf820932c5e6cb" + +[[package]] +name = "polyval" +version = "0.4.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eebcc4aa140b9abd2bc40d9c3f7ccec842679cd79045ac3a7ac698c1a064b7cd" +dependencies = [ + "cpuid-bool", + "opaque-debug", + "universal-hash", +] + +[[package]] +name = "pow_sha256" +version = "0.2.1" +source = "git+https://github.com/mcaptcha/pow_sha256#807fa7c75284f6d8d488a6f66a3a1b3301f2f412" +dependencies = [ + "bincode", + "derive_builder", + "num", + "serde 1.0.130", + "sha2", +] + +[[package]] +name = "ppv-lite86" +version = "0.2.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac74c624d6b2d21f425f752262f42188365d7b8ff1aff74c82e45136510a4857" + +[[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-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-macro-nested" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bc881b2c22681370c6a780e47af9840ef841837bc98118431d4e1868bd0c1086" + +[[package]] +name = "proc-macro2" +version = "1.0.26" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a152013215dca273577e18d2bf00fa862b89b24169fb78c4c95aeb07992c9cec" +dependencies = [ + "unicode-xid", +] + +[[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.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3d0b9745dc2debf507c8422de05d7226cc1f0644216dfdfead988f9b1ab32a7" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "rand" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2e7573632e6454cf6b99d7aac4ccca54be06da05aca2ef7423d22d27d4d4bcd8" +dependencies = [ + "libc", + "rand_chacha", + "rand_core", + "rand_hc", +] + +[[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", +] + +[[package]] +name = "rand_core" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d34f1408f55294453790c48b2f1ebbb1c5b4b7563eb1f418bcfcfdbb06ebb4e7" +dependencies = [ + "getrandom", +] + +[[package]] +name = "rand_hc" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d51e9f596de227fda2ea6c84607f5558e196eeaf43c986b724ba4fb8fdf497e7" +dependencies = [ + "rand_core", +] + +[[package]] +name = "redis" +version = "0.21.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "202c5bf92cad3d57605c366e644a7fbf305a83f19754fc66678c6265dcc9b8b4" +dependencies = [ + "async-trait", + "bytes", + "combine", + "crc16", + "dtoa", + "futures-util", + "itoa", + "percent-encoding", + "pin-project-lite", + "rand", + "sha1", + "tokio", + "tokio-util", + "url", +] + +[[package]] +name = "redox_syscall" +version = "0.2.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8383f39639269cde97d255a32bdb68c047337295414940c68bdd30c2e13203ff" +dependencies = [ + "bitflags", +] + +[[package]] +name = "redox_users" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "528532f3d801c87aec9def2add9ca802fe569e44a544afe633765267840abe64" +dependencies = [ + "getrandom", + "redox_syscall", +] + +[[package]] +name = "regex" +version = "1.5.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d07a8629359eb56f1e2fb1652bb04212c072a87ba68546a04065d525673ac461" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax", +] + +[[package]] +name = "regex-syntax" +version = "0.6.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f497285884f3fcff424ffc933e56d7cbca511def0c9831a7f9b5f6153e3cc89b" + +[[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", + "untrusted", + "web-sys", + "winapi", +] + +[[package]] +name = "rust-embed" +version = "6.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1be44a6694859b7cfc955699935944a6844aa9fe416aeda5d40829e3e38dfee6" +dependencies = [ + "rust-embed-impl", + "rust-embed-utils", + "walkdir", +] + +[[package]] +name = "rust-embed-impl" +version = "6.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f567ca01565c50c67b29e535f5f67b8ea8aeadaeed16a88f10792ab57438b957" +dependencies = [ + "proc-macro2", + "quote", + "rust-embed-utils", + "syn", + "walkdir", +] + +[[package]] +name = "rust-embed-utils" +version = "7.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6116e7ab9ea963f60f2f20291d8fcf6c7273192cdd7273b3c80729a9605c97b2" +dependencies = [ + "sha2", + "walkdir", +] + +[[package]] +name = "rust-ini" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3e52c148ef37f8c375d49d5a73aa70713125b7f19095948a923f80afdeb22ec2" + +[[package]] +name = "rustc_version" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "138e3e0acb6c9fb258b19b67cb8abd63c00679d2851805ea151465464fe9030a" +dependencies = [ + "semver 0.9.0", +] + +[[package]] +name = "rustc_version" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0dfe2087c51c460008730de8b57e6a320782fbfb312e1f4d520e6c6fae155ee" +dependencies = [ + "semver 0.11.0", +] + +[[package]] +name = "rustls" +version = "0.19.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "35edb675feee39aec9c99fa5ff985081995a06d594114ae14cbe797ad7b7a6d7" +dependencies = [ + "base64", + "log", + "ring", + "sct", + "webpki", +] + +[[package]] +name = "ryu" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "71d301d4193d031abdd79ff7e3dd721168a9572ef3fe51a1517aba235bd8f86e" + +[[package]] +name = "sailfish" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "816920a08514d9741242b3efe70c16c350ed548bc4a5ba03426e56faf9d45f77" +dependencies = [ + "itoap", + "ryu", + "sailfish-macros", + "version_check", +] + +[[package]] +name = "sailfish-compiler" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4276e7b848bde8e7813d534f014bc35ce5acd2b9e2b6b075727113fcf478ba63" +dependencies = [ + "filetime", + "home", + "memchr", + "proc-macro2", + "quote", + "syn", + "yaml-rust", +] + +[[package]] +name = "sailfish-macros" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6bba2458ef07ae12c9aed2edb866c3db2f9c21cf19a2c3f2613b2982bc1a4a46" +dependencies = [ + "proc-macro2", + "sailfish-compiler", +] + +[[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 = "scopeguard" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d29ab0c6d3fc0ee92fe66e2d99f700eab17a8d57d1c1d3b748380fb20baa78cd" + +[[package]] +name = "sct" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b362b83898e0e69f38515b82ee15aa80636befe47c3b6d3d89a911e78fc228ce" +dependencies = [ + "ring", + "untrusted", +] + +[[package]] +name = "semver" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d7eb9ef2c18661902cc47e535f9bc51b78acd254da71d375c2f6720d9a40403" +dependencies = [ + "semver-parser 0.7.0", +] + +[[package]] +name = "semver" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f301af10236f6df4160f7c3f04eec6dbc70ace82d23326abad5edee88801c6b6" +dependencies = [ + "semver-parser 0.10.2", +] + +[[package]] +name = "semver-parser" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "388a1df253eca08550bef6c72392cfe7c30914bf41df5269b68cbd6ff8f570a3" + +[[package]] +name = "semver-parser" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "00b0bef5b7f9e0df16536d3961cfb6e84331c065b4066afb39768d0e319411f7" +dependencies = [ + "pest", +] + +[[package]] +name = "serde" +version = "0.8.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9dad3f759919b92c3068c696c15c3d17238234498bbdcc80f2c469606f948ac8" + +[[package]] +name = "serde" +version = "1.0.130" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f12d06de37cf59146fbdecab66aa99f9fe4f78722e3607577a5375d66bd0c913" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde-hjson" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6a3a4e0ea8a88553209f6cc6cfe8724ecad22e1acf372793c27d995290fe74f8" +dependencies = [ + "lazy_static", + "num-traits 0.1.43", + "regex", + "serde 0.8.23", +] + +[[package]] +name = "serde_derive" +version = "1.0.130" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d7bc1a1ab1961464eae040d96713baa5a724a8152c1222492465b54322ec508b" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "serde_json" +version = "1.0.68" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0f690853975602e1bfe1ccbf50504d67174e3bcf340f23b5ea9992e0587a52d8" +dependencies = [ + "indexmap", + "itoa", + "ryu", + "serde 1.0.130", +] + +[[package]] +name = "serde_urlencoded" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "edfa57a7f8d9c1d260a549e7224100f6c43d43f9103e06dd8b4095a9b2b43ce9" +dependencies = [ + "form_urlencoded", + "itoa", + "ryu", + "serde 1.0.130", +] + +[[package]] +name = "sha-1" +version = "0.9.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "99cd6713db3cf16b6c84e06321e049a9b9f699826e16096d23bbcc44d15d51a6" +dependencies = [ + "block-buffer", + "cfg-if", + "cpufeatures", + "digest", + "opaque-debug", +] + +[[package]] +name = "sha1" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2579985fda508104f7587689507983eadd6a6e84dd35d6d115361f530916fa0d" + +[[package]] +name = "sha2" +version = "0.9.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b69f9a4c9740d74c5baa3fd2e547f9525fa8088a8a958e0ca2409a514e33f5fa" +dependencies = [ + "block-buffer", + "cfg-if", + "cpufeatures", + "digest", + "opaque-debug", +] + +[[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 = "slab" +version = "0.4.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c307a32c1c5c437f38c7fd45d753050587732ba8628319fbdf12a7e289ccc590" + +[[package]] +name = "smallvec" +version = "1.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ecab6c735a6bb4139c0caafd0cc3635748bbb3acf4550e8138122099251f309" + +[[package]] +name = "socket2" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5dc90fe6c7be1a323296982db1836d1ea9e47b6839496dde9a541bc496df3516" +dependencies = [ + "libc", + "winapi", +] + +[[package]] +name = "spin" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e63cff320ae2c57904679ba7cb63280a3dc4613885beafb148ee7bf9aa9042d" + +[[package]] +name = "sqlformat" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b4b7922be017ee70900be125523f38bdd644f4f06a1b16e8fa5a8ee8c34bffd4" +dependencies = [ + "itertools", + "nom 7.0.0", + "unicode_categories", +] + +[[package]] +name = "sqlx" +version = "0.5.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0e4b94ab0f8c21ee4899b93b06451ef5d965f1a355982ee73684338228498440" +dependencies = [ + "sqlx-core", + "sqlx-macros", +] + +[[package]] +name = "sqlx-core" +version = "0.5.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec28b91a01e1fe286d6ba66f68289a2286df023fc97444e1fd86c2fd6d5dc026" +dependencies = [ + "ahash", + "atoi", + "base64", + "bitflags", + "byteorder", + "bytes", + "crc", + "crossbeam-channel", + "crossbeam-queue", + "crossbeam-utils", + "dirs", + "either", + "futures-channel", + "futures-core", + "futures-intrusive", + "futures-util", + "hashlink", + "hex", + "hmac", + "itoa", + "libc", + "log", + "md-5", + "memchr", + "once_cell", + "parking_lot", + "percent-encoding", + "rand", + "rustls", + "serde 1.0.130", + "serde_json", + "sha-1", + "sha2", + "smallvec", + "sqlformat", + "sqlx-rt", + "stringprep", + "thiserror", + "time", + "tokio-stream", + "url", + "uuid", + "webpki", + "webpki-roots", + "whoami", +] + +[[package]] +name = "sqlx-macros" +version = "0.5.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4dc33c35d54774eed73d54568d47a6ac099aed8af5e1556a017c131be88217d5" +dependencies = [ + "dotenv", + "either", + "futures", + "heck", + "hex", + "once_cell", + "proc-macro2", + "quote", + "serde 1.0.130", + "serde_json", + "sha2", + "sqlx-core", + "sqlx-rt", + "syn", + "url", +] + +[[package]] +name = "sqlx-rt" +version = "0.5.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "14302b678d9c76b28f2e60115211e25e0aabc938269991745a169753dc00e35c" +dependencies = [ + "actix-rt", + "once_cell", + "tokio", + "tokio-rustls", +] + +[[package]] +name = "standback" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e113fb6f3de07a243d434a56ec6f186dfd51cb08448239fe7bcae73f87ff28ff" +dependencies = [ + "version_check", +] + +[[package]] +name = "static_assertions" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" + +[[package]] +name = "stdweb" +version = "0.4.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d022496b16281348b52d0e30ae99e01a73d737b2f45d38fed4edf79f9325a1d5" +dependencies = [ + "discard", + "rustc_version 0.2.3", + "stdweb-derive", + "stdweb-internal-macros", + "stdweb-internal-runtime", + "wasm-bindgen", +] + +[[package]] +name = "stdweb-derive" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c87a60a40fccc84bef0652345bbbbbe20a605bf5d0ce81719fc476f5c03b50ef" +dependencies = [ + "proc-macro2", + "quote", + "serde 1.0.130", + "serde_derive", + "syn", +] + +[[package]] +name = "stdweb-internal-macros" +version = "0.2.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "58fa5ff6ad0d98d1ffa8cb115892b6e69d67799f6763e162a1c9db421dc22e11" +dependencies = [ + "base-x", + "proc-macro2", + "quote", + "serde 1.0.130", + "serde_derive", + "serde_json", + "sha1", + "syn", +] + +[[package]] +name = "stdweb-internal-runtime" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "213701ba3370744dcd1a12960caa4843b3d68b4d1c0a5d575e0d65b2ee9d16c0" + +[[package]] +name = "stringprep" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ee348cb74b87454fff4b551cbf727025810a004f88aeacae7f85b87f4e9a1c1" +dependencies = [ + "unicode-bidi", + "unicode-normalization", +] + +[[package]] +name = "strsim" +version = "0.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 = "survey" +version = "0.1.0" +dependencies = [ + "actix", + "actix-cors", + "actix-http", + "actix-identity", + "actix-rt", + "actix-service", + "actix-web", + "actix-web-codegen 0.5.0-beta.4 (git+https://github.com/realaravinth/actix-web)", + "cache-buster", + "config", + "derive_builder", + "derive_more", + "futures", + "lazy_static", + "libmcaptcha", + "log", + "mime", + "mime_guess", + "openssl", + "pretty_env_logger", + "rand", + "rust-embed", + "sailfish", + "serde 1.0.130", + "serde_json", + "sqlx", + "tokio", + "url", + "uuid", + "validator", +] + +[[package]] +name = "syn" +version = "1.0.77" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5239bc68e0fef57495900cfea4e8dc75596d9a319d7e16b1e0a440d24e6fe0a0" +dependencies = [ + "proc-macro2", + "quote", + "unicode-xid", +] + +[[package]] +name = "termcolor" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2dfed899f0eb03f32ee8c6a0aabdb8a7949659e3466561fc0adf54e26d88c5f4" +dependencies = [ + "winapi-util", +] + +[[package]] +name = "thiserror" +version = "1.0.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "602eca064b2d83369e2b2f34b09c70b605402801927c65c11071ac911d299b88" +dependencies = [ + "thiserror-impl", +] + +[[package]] +name = "thiserror-impl" +version = "1.0.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bad553cc2c78e8de258400763a647e80e6d1b31ee237275d756f6836d204494c" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "time" +version = "0.2.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4752a97f8eebd6854ff91f1c1824cd6160626ac4bd44287f7f4ea2035a02a242" +dependencies = [ + "const_fn", + "libc", + "standback", + "stdweb", + "time-macros", + "version_check", + "winapi", +] + +[[package]] +name = "time-macros" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "957e9c6e26f12cb6d0dd7fc776bb67a706312e7299aed74c8dd5b17ebb27e2f1" +dependencies = [ + "proc-macro-hack", + "time-macros-impl", +] + +[[package]] +name = "time-macros-impl" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fd3c141a1b43194f3f56a1411225df8646c55781d5f26db825b3d98507eb482f" +dependencies = [ + "proc-macro-hack", + "proc-macro2", + "quote", + "standback", + "syn", +] + +[[package]] +name = "tinyvec" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f83b2a3d4d9091d0abd7eba4dc2710b1718583bd4d8992e2190720ea38f391f7" +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.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2c2416fdedca8443ae44b4527de1ea633af61d8f7169ffa6e72c5b53d24efcc" +dependencies = [ + "autocfg", + "bytes", + "libc", + "memchr", + "mio", + "num_cpus", + "once_cell", + "parking_lot", + "pin-project-lite", + "signal-hook-registry", + "winapi", +] + +[[package]] +name = "tokio-rustls" +version = "0.22.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bc6844de72e57df1980054b38be3a9f4702aba4858be64dd700181a8a6d0e1b6" +dependencies = [ + "rustls", + "tokio", + "webpki", +] + +[[package]] +name = "tokio-stream" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7b2f3f698253f03119ac0102beaa64f67a67e08074d03a22d18784104543727f" +dependencies = [ + "futures-core", + "pin-project-lite", + "tokio", +] + +[[package]] +name = "tokio-util" +version = "0.6.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08d3725d3efa29485e87311c5b699de63cde14b00ed4d256b8318aa30ca452cd" +dependencies = [ + "bytes", + "futures-core", + "futures-sink", + "log", + "pin-project-lite", + "tokio", +] + +[[package]] +name = "toml" +version = "0.5.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a31142970826733df8241ef35dc040ef98c679ab14d7c3e54d827099b3acecaa" +dependencies = [ + "serde 1.0.130", +] + +[[package]] +name = "tracing" +version = "0.1.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "84f96e095c0c82419687c20ddf5cb3eadb61f4e1405923c9dc8e53a1adacbda8" +dependencies = [ + "cfg-if", + "pin-project-lite", + "tracing-core", +] + +[[package]] +name = "tracing-core" +version = "0.1.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "46125608c26121c81b0c6d693eab5a420e416da7e43c426d2e8f7df8da8a3acf" +dependencies = [ + "lazy_static", +] + +[[package]] +name = "typenum" +version = "1.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b63708a265f51345575b27fe43f9500ad611579e764c79edbc2037b1121959ec" + +[[package]] +name = "ucd-trie" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56dee185309b50d1f11bfedef0fe6d036842e3fb77413abef29f8f8d1c5d4c1c" + +[[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.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "246f4c42e67e7a4e3c6106ff716a5d067d4132a642840b242e357e468a2a0085" + +[[package]] +name = "unicode-normalization" +version = "0.1.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d54590932941a9e9266f0832deed84ebe1bf2e4c9e4a3554d393d18f5e854bf9" +dependencies = [ + "tinyvec", +] + +[[package]] +name = "unicode-segmentation" +version = "1.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8895849a949e7845e06bd6dc1aa51731a103c42707010a5b591c0038fb73385b" + +[[package]] +name = "unicode-xid" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ccb82d61f80a663efe1f787a51b16b5a51e3314d6ac365b08639f52387b33f3" + +[[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.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f214e8f697e925001e66ec2c6e37a4ef93f0f78c2eed7814394e10c62025b05" +dependencies = [ + "generic-array", + "subtle", +] + +[[package]] +name = "untrusted" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a156c684c91ea7d62626509bce3cb4e1d9ed5c4d978f7b4352658f96a4c26b4a" + +[[package]] +name = "url" +version = "2.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a507c383b2d33b5fc35d1861e77e6b383d158b2da5e14fe51b83dfedf6fd578c" +dependencies = [ + "form_urlencoded", + "idna", + "matches", + "percent-encoding", +] + +[[package]] +name = "uuid" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bc5cf98d8186244414c848017f0e2676b3fcb46807f6668a97dfe67359a3c4b7" +dependencies = [ + "getrandom", +] + +[[package]] +name = "validator" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6d0f08911ab0fee2c5009580f04615fa868898ee57de10692a45da0c3bcc3e5e" +dependencies = [ + "idna", + "lazy_static", + "regex", + "serde 1.0.130", + "serde_derive", + "serde_json", + "url", + "validator_derive", + "validator_types", +] + +[[package]] +name = "validator_derive" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d85135714dba11a1bd0b3eb1744169266f1a38977bf4e3ff5e2e1acb8c2b7eee" +dependencies = [ + "if_chain", + "lazy_static", + "proc-macro-error", + "proc-macro2", + "quote", + "regex", + "syn", + "validator_types", +] + +[[package]] +name = "validator_types" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ded9d97e1d42327632f5f3bae6403c04886e2de3036261ef42deebd931a6a291" +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.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5fecdca9a5291cc2b8dcf7dc02453fee791a280f3743cb0905f8822ae463b3fe" + +[[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 = "wasi" +version = "0.10.2+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fd6fbd9a79829dd1ad0cc20627bf1ed606756a7f77edff7b66b7064f9cb327c6" + +[[package]] +name = "wasm-bindgen" +version = "0.2.78" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "632f73e236b219150ea279196e54e610f5dbafa5d61786303d4da54f84e47fce" +dependencies = [ + "cfg-if", + "wasm-bindgen-macro", +] + +[[package]] +name = "wasm-bindgen-backend" +version = "0.2.78" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a317bf8f9fba2476b4b2c85ef4c4af8ff39c3c7f0cdfeed4f82c34a880aa837b" +dependencies = [ + "bumpalo", + "lazy_static", + "log", + "proc-macro2", + "quote", + "syn", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.78" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d56146e7c495528bf6587663bea13a8eb588d39b36b679d83972e1a2dbbdacf9" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.78" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7803e0eea25835f8abdc585cd3021b3deb11543c6fe226dcd30b228857c5c5ab" +dependencies = [ + "proc-macro2", + "quote", + "syn", + "wasm-bindgen-backend", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.78" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0237232789cf037d5480773fe568aac745bfe2afbc11a863e97901780a6b47cc" + +[[package]] +name = "web-sys" +version = "0.3.55" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38eb105f1c59d9eaa6b5cdc92b859d85b926e82cb2e0945cd0c9259faa6fe9fb" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "webpki" +version = "0.21.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8e38c0608262c46d4a56202ebabdeb094cef7e560ca7a226c6bf055188aa4ea" +dependencies = [ + "ring", + "untrusted", +] + +[[package]] +name = "webpki-roots" +version = "0.21.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aabe153544e473b775453675851ecc86863d2a81d786d741f6b76778f2a48940" +dependencies = [ + "webpki", +] + +[[package]] +name = "whoami" +version = "1.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "483a59fee1a93fec90eb08bc2eb4315ef10f4ebc478b3a5fadc969819cb66117" +dependencies = [ + "wasm-bindgen", + "web-sys", +] + +[[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 = "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.7.0+zstd.1.4.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9428752481d8372e15b1bf779ea518a179ad6c771cca2d2c60e4fbff3cc2cd52" +dependencies = [ + "zstd-safe", +] + +[[package]] +name = "zstd-safe" +version = "3.1.0+zstd.1.4.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5aa1926623ad7fe406e090555387daf73db555b948134b4d73eac5eb08fb666d" +dependencies = [ + "libc", + "zstd-sys", +] + +[[package]] +name = "zstd-sys" +version = "1.5.0+zstd.1.4.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4e6c094340240369025fc6b731b054ee2a834328fa584310ac96aa4baebdc465" +dependencies = [ + "cc", + "libc", +] diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..cc67599 --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,76 @@ +[package] +name = "survey" +version = "0.1.0" +description = "Feedback agregator" +homepage = "https://github.com/mCaptcha/survey" +repository = "https://github.com/mCaptcha/survey" +documentation = "https://github.con/mCaptcha/survey" +readme = "https://github.com/mCaptcha/survey/blob/master/README.md" +license = "AGPLv3 or later version" +authors = ["realaravinth "] +edition = "2018" +default-run = "survey" +build = "build.rs" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html +[[bin]] +name = "survey" +path = "./src/main.rs" + +[[bin]] +name = "tests-migrate" +path = "./src/tests-migrate.rs" + +[dependencies] +actix-web = "4.0.0-beta.9" +actix-identity = "0.4.0-beta.2" +actix-http = "3.0.0-beta.8" +actix-rt = "2" +actix-cors = "0.6.0-beta.2" +actix-service = "2.0.0" +actix = "0.12" +my-codegen = {package = "actix-web-codegen", git ="https://github.com/realaravinth/actix-web"} + +libmcaptcha = { branch = "master", git = "https://github.com/mCaptcha/libmcaptcha", features = ["full"] } + +futures = "0.3.15" + +sqlx = { version = "0.5.5", features = [ "runtime-actix-rustls", "postgres", "time", "offline" ] } + +derive_builder = "0.10" +validator = { version = "0.14", features = ["derive"]} +derive_more = "0.99" + +config = "0.11" + +serde = "1" +serde_json = "1" + +pretty_env_logger = "0.4" +log = "0.4" + +lazy_static = "1.4" + +url = "2.2" + +rand = "0.8" +uuid = { version="0.8.2", features = ["v4"]} + +mime_guess = "2.0.3" +rust-embed = "6.0.0" +cache-buster = { git = "https://github.com/realaravinth/cache-buster" } +mime = "0.3.16" + +openssl = { version = "0.10.29", features = ["vendored"] } + +sailfish = "0.3.2" + +tokio = "1.11.0" + +[build-dependencies] +sqlx = { version = "0.5.5", features = [ "runtime-actix-rustls", "uuid", "postgres", "time", "offline" ] } +#serde_yaml = "0.8.17" +serde_json = "1" +#yaml-rust = "0.4.5" +cache-buster = { version = "0.2.0", git = "https://github.com/realaravinth/cache-buster" } +mime = "0.3.16" diff --git a/LICENSE.md b/LICENSE.md new file mode 100644 index 0000000..cba6f6a --- /dev/null +++ b/LICENSE.md @@ -0,0 +1,660 @@ +### GNU AFFERO GENERAL PUBLIC LICENSE + +Version 3, 19 November 2007 + +Copyright (C) 2007 Free Software Foundation, Inc. + + +Everyone is permitted to copy and distribute verbatim copies of this +license document, but changing it is not allowed. + +### Preamble + +The GNU Affero General Public License is a free, copyleft license for +software and other kinds of works, specifically designed to ensure +cooperation with the community in the case of network server software. + +The licenses for most software and other practical works are designed +to take away your freedom to share and change the works. By contrast, +our General Public Licenses are intended to guarantee your freedom to +share and change all versions of a program--to make sure it remains +free software for all its users. + +When we speak of free software, we are referring to freedom, not +price. Our General Public Licenses are designed to make sure that you +have the freedom to distribute copies of free software (and charge for +them if you wish), that you receive source code or can get it if you +want it, that you can change the software or use pieces of it in new +free programs, and that you know you can do these things. + +Developers that use our General Public Licenses protect your rights +with two steps: (1) assert copyright on the software, and (2) offer +you this License which gives you legal permission to copy, distribute +and/or modify the software. + +A secondary benefit of defending all users' freedom is that +improvements made in alternate versions of the program, if they +receive widespread use, become available for other developers to +incorporate. Many developers of free software are heartened and +encouraged by the resulting cooperation. However, in the case of +software used on network servers, this result may fail to come about. +The GNU General Public License permits making a modified version and +letting the public access it on a server without ever releasing its +source code to the public. + +The GNU Affero General Public License is designed specifically to +ensure that, in such cases, the modified source code becomes available +to the community. It requires the operator of a network server to +provide the source code of the modified version running there to the +users of that server. Therefore, public use of a modified version, on +a publicly accessible server, gives the public access to the source +code of the modified version. + +An older license, called the Affero General Public License and +published by Affero, was designed to accomplish similar goals. This is +a different license, not a version of the Affero GPL, but Affero has +released a new version of the Affero GPL which permits relicensing +under this license. + +The precise terms and conditions for copying, distribution and +modification follow. + +### TERMS AND CONDITIONS + +#### 0. Definitions. + +"This License" refers to version 3 of the GNU Affero General Public +License. + +"Copyright" also means copyright-like laws that apply to other kinds +of works, such as semiconductor masks. + +"The Program" refers to any copyrightable work licensed under this +License. Each licensee is addressed as "you". "Licensees" and +"recipients" may be individuals or organizations. + +To "modify" a work means to copy from or adapt all or part of the work +in a fashion requiring copyright permission, other than the making of +an exact copy. The resulting work is called a "modified version" of +the earlier work or a work "based on" the earlier work. + +A "covered work" means either the unmodified Program or a work based +on the Program. + +To "propagate" a work means to do anything with it that, without +permission, would make you directly or secondarily liable for +infringement under applicable copyright law, except executing it on a +computer or modifying a private copy. Propagation includes copying, +distribution (with or without modification), making available to the +public, and in some countries other activities as well. + +To "convey" a work means any kind of propagation that enables other +parties to make or receive copies. Mere interaction with a user +through a computer network, with no transfer of a copy, is not +conveying. + +An interactive user interface displays "Appropriate Legal Notices" to +the extent that it includes a convenient and prominently visible +feature that (1) displays an appropriate copyright notice, and (2) +tells the user that there is no warranty for the work (except to the +extent that warranties are provided), that licensees may convey the +work under this License, and how to view a copy of this License. If +the interface presents a list of user commands or options, such as a +menu, a prominent item in the list meets this criterion. + +#### 1. Source Code. + +The "source code" for a work means the preferred form of the work for +making modifications to it. "Object code" means any non-source form of +a work. + +A "Standard Interface" means an interface that either is an official +standard defined by a recognized standards body, or, in the case of +interfaces specified for a particular programming language, one that +is widely used among developers working in that language. + +The "System Libraries" of an executable work include anything, other +than the work as a whole, that (a) is included in the normal form of +packaging a Major Component, but which is not part of that Major +Component, and (b) serves only to enable use of the work with that +Major Component, or to implement a Standard Interface for which an +implementation is available to the public in source code form. A +"Major Component", in this context, means a major essential component +(kernel, window system, and so on) of the specific operating system +(if any) on which the executable work runs, or a compiler used to +produce the work, or an object code interpreter used to run it. + +The "Corresponding Source" for a work in object code form means all +the source code needed to generate, install, and (for an executable +work) run the object code and to modify the work, including scripts to +control those activities. However, it does not include the work's +System Libraries, or general-purpose tools or generally available free +programs which are used unmodified in performing those activities but +which are not part of the work. For example, Corresponding Source +includes interface definition files associated with source files for +the work, and the source code for shared libraries and dynamically +linked subprograms that the work is specifically designed to require, +such as by intimate data communication or control flow between those +subprograms and other parts of the work. + +The Corresponding Source need not include anything that users can +regenerate automatically from other parts of the Corresponding Source. + +The Corresponding Source for a work in source code form is that same +work. + +#### 2. Basic Permissions. + +All rights granted under this License are granted for the term of +copyright on the Program, and are irrevocable provided the stated +conditions are met. This License explicitly affirms your unlimited +permission to run the unmodified Program. The output from running a +covered work is covered by this License only if the output, given its +content, constitutes a covered work. This License acknowledges your +rights of fair use or other equivalent, as provided by copyright law. + +You may make, run and propagate covered works that you do not convey, +without conditions so long as your license otherwise remains in force. +You may convey covered works to others for the sole purpose of having +them make modifications exclusively for you, or provide you with +facilities for running those works, provided that you comply with the +terms of this License in conveying all material for which you do not +control copyright. Those thus making or running the covered works for +you must do so exclusively on your behalf, under your direction and +control, on terms that prohibit them from making any copies of your +copyrighted material outside their relationship with you. + +Conveying under any other circumstances is permitted solely under the +conditions stated below. Sublicensing is not allowed; section 10 makes +it unnecessary. + +#### 3. Protecting Users' Legal Rights From Anti-Circumvention Law. + +No covered work shall be deemed part of an effective technological +measure under any applicable law fulfilling obligations under article +11 of the WIPO copyright treaty adopted on 20 December 1996, or +similar laws prohibiting or restricting circumvention of such +measures. + +When you convey a covered work, you waive any legal power to forbid +circumvention of technological measures to the extent such +circumvention is effected by exercising rights under this License with +respect to the covered work, and you disclaim any intention to limit +operation or modification of the work as a means of enforcing, against +the work's users, your or third parties' legal rights to forbid +circumvention of technological measures. + +#### 4. Conveying Verbatim Copies. + +You may convey verbatim copies of the Program's source code as you +receive it, in any medium, provided that you conspicuously and +appropriately publish on each copy an appropriate copyright notice; +keep intact all notices stating that this License and any +non-permissive terms added in accord with section 7 apply to the code; +keep intact all notices of the absence of any warranty; and give all +recipients a copy of this License along with the Program. + +You may charge any price or no price for each copy that you convey, +and you may offer support or warranty protection for a fee. + +#### 5. Conveying Modified Source Versions. + +You may convey a work based on the Program, or the modifications to +produce it from the Program, in the form of source code under the +terms of section 4, provided that you also meet all of these +conditions: + +- a) The work must carry prominent notices stating that you modified + it, and giving a relevant date. +- b) The work must carry prominent notices stating that it is + released under this License and any conditions added under + section 7. This requirement modifies the requirement in section 4 + to "keep intact all notices". +- c) You must license the entire work, as a whole, under this + License to anyone who comes into possession of a copy. This + License will therefore apply, along with any applicable section 7 + additional terms, to the whole of the work, and all its parts, + regardless of how they are packaged. This License gives no + permission to license the work in any other way, but it does not + invalidate such permission if you have separately received it. +- d) If the work has interactive user interfaces, each must display + Appropriate Legal Notices; however, if the Program has interactive + interfaces that do not display Appropriate Legal Notices, your + work need not make them do so. + +A compilation of a covered work with other separate and independent +works, which are not by their nature extensions of the covered work, +and which are not combined with it such as to form a larger program, +in or on a volume of a storage or distribution medium, is called an +"aggregate" if the compilation and its resulting copyright are not +used to limit the access or legal rights of the compilation's users +beyond what the individual works permit. Inclusion of a covered work +in an aggregate does not cause this License to apply to the other +parts of the aggregate. + +#### 6. Conveying Non-Source Forms. + +You may convey a covered work in object code form under the terms of +sections 4 and 5, provided that you also convey the machine-readable +Corresponding Source under the terms of this License, in one of these +ways: + +- a) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by the + Corresponding Source fixed on a durable physical medium + customarily used for software interchange. +- b) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by a + written offer, valid for at least three years and valid for as + long as you offer spare parts or customer support for that product + model, to give anyone who possesses the object code either (1) a + copy of the Corresponding Source for all the software in the + product that is covered by this License, on a durable physical + medium customarily used for software interchange, for a price no + more than your reasonable cost of physically performing this + conveying of source, or (2) access to copy the Corresponding + Source from a network server at no charge. +- c) Convey individual copies of the object code with a copy of the + written offer to provide the Corresponding Source. This + alternative is allowed only occasionally and noncommercially, and + only if you received the object code with such an offer, in accord + with subsection 6b. +- d) Convey the object code by offering access from a designated + place (gratis or for a charge), and offer equivalent access to the + Corresponding Source in the same way through the same place at no + further charge. You need not require recipients to copy the + Corresponding Source along with the object code. If the place to + copy the object code is a network server, the Corresponding Source + may be on a different server (operated by you or a third party) + that supports equivalent copying facilities, provided you maintain + clear directions next to the object code saying where to find the + Corresponding Source. Regardless of what server hosts the + Corresponding Source, you remain obligated to ensure that it is + available for as long as needed to satisfy these requirements. +- e) Convey the object code using peer-to-peer transmission, + provided you inform other peers where the object code and + Corresponding Source of the work are being offered to the general + public at no charge under subsection 6d. + +A separable portion of the object code, whose source code is excluded +from the Corresponding Source as a System Library, need not be +included in conveying the object code work. + +A "User Product" is either (1) a "consumer product", which means any +tangible personal property which is normally used for personal, +family, or household purposes, or (2) anything designed or sold for +incorporation into a dwelling. In determining whether a product is a +consumer product, doubtful cases shall be resolved in favor of +coverage. For a particular product received by a particular user, +"normally used" refers to a typical or common use of that class of +product, regardless of the status of the particular user or of the way +in which the particular user actually uses, or expects or is expected +to use, the product. A product is a consumer product regardless of +whether the product has substantial commercial, industrial or +non-consumer uses, unless such uses represent the only significant +mode of use of the product. + +"Installation Information" for a User Product means any methods, +procedures, authorization keys, or other information required to +install and execute modified versions of a covered work in that User +Product from a modified version of its Corresponding Source. The +information must suffice to ensure that the continued functioning of +the modified object code is in no case prevented or interfered with +solely because modification has been made. + +If you convey an object code work under this section in, or with, or +specifically for use in, a User Product, and the conveying occurs as +part of a transaction in which the right of possession and use of the +User Product is transferred to the recipient in perpetuity or for a +fixed term (regardless of how the transaction is characterized), the +Corresponding Source conveyed under this section must be accompanied +by the Installation Information. But this requirement does not apply +if neither you nor any third party retains the ability to install +modified object code on the User Product (for example, the work has +been installed in ROM). + +The requirement to provide Installation Information does not include a +requirement to continue to provide support service, warranty, or +updates for a work that has been modified or installed by the +recipient, or for the User Product in which it has been modified or +installed. Access to a network may be denied when the modification +itself materially and adversely affects the operation of the network +or violates the rules and protocols for communication across the +network. + +Corresponding Source conveyed, and Installation Information provided, +in accord with this section must be in a format that is publicly +documented (and with an implementation available to the public in +source code form), and must require no special password or key for +unpacking, reading or copying. + +#### 7. Additional Terms. + +"Additional permissions" are terms that supplement the terms of this +License by making exceptions from one or more of its conditions. +Additional permissions that are applicable to the entire Program shall +be treated as though they were included in this License, to the extent +that they are valid under applicable law. If additional permissions +apply only to part of the Program, that part may be used separately +under those permissions, but the entire Program remains governed by +this License without regard to the additional permissions. + +When you convey a copy of a covered work, you may at your option +remove any additional permissions from that copy, or from any part of +it. (Additional permissions may be written to require their own +removal in certain cases when you modify the work.) You may place +additional permissions on material, added by you to a covered work, +for which you have or can give appropriate copyright permission. + +Notwithstanding any other provision of this License, for material you +add to a covered work, you may (if authorized by the copyright holders +of that material) supplement the terms of this License with terms: + +- a) Disclaiming warranty or limiting liability differently from the + terms of sections 15 and 16 of this License; or +- b) Requiring preservation of specified reasonable legal notices or + author attributions in that material or in the Appropriate Legal + Notices displayed by works containing it; or +- c) Prohibiting misrepresentation of the origin of that material, + or requiring that modified versions of such material be marked in + reasonable ways as different from the original version; or +- d) Limiting the use for publicity purposes of names of licensors + or authors of the material; or +- e) Declining to grant rights under trademark law for use of some + trade names, trademarks, or service marks; or +- f) Requiring indemnification of licensors and authors of that + material by anyone who conveys the material (or modified versions + of it) with contractual assumptions of liability to the recipient, + for any liability that these contractual assumptions directly + impose on those licensors and authors. + +All other non-permissive additional terms are considered "further +restrictions" within the meaning of section 10. If the Program as you +received it, or any part of it, contains a notice stating that it is +governed by this License along with a term that is a further +restriction, you may remove that term. If a license document contains +a further restriction but permits relicensing or conveying under this +License, you may add to a covered work material governed by the terms +of that license document, provided that the further restriction does +not survive such relicensing or conveying. + +If you add terms to a covered work in accord with this section, you +must place, in the relevant source files, a statement of the +additional terms that apply to those files, or a notice indicating +where to find the applicable terms. + +Additional terms, permissive or non-permissive, may be stated in the +form of a separately written license, or stated as exceptions; the +above requirements apply either way. + +#### 8. Termination. + +You may not propagate or modify a covered work except as expressly +provided under this License. Any attempt otherwise to propagate or +modify it is void, and will automatically terminate your rights under +this License (including any patent licenses granted under the third +paragraph of section 11). + +However, if you cease all violation of this License, then your license +from a particular copyright holder is reinstated (a) provisionally, +unless and until the copyright holder explicitly and finally +terminates your license, and (b) permanently, if the copyright holder +fails to notify you of the violation by some reasonable means prior to +60 days after the cessation. + +Moreover, your license from a particular copyright holder is +reinstated permanently if the copyright holder notifies you of the +violation by some reasonable means, this is the first time you have +received notice of violation of this License (for any work) from that +copyright holder, and you cure the violation prior to 30 days after +your receipt of the notice. + +Termination of your rights under this section does not terminate the +licenses of parties who have received copies or rights from you under +this License. If your rights have been terminated and not permanently +reinstated, you do not qualify to receive new licenses for the same +material under section 10. + +#### 9. Acceptance Not Required for Having Copies. + +You are not required to accept this License in order to receive or run +a copy of the Program. Ancillary propagation of a covered work +occurring solely as a consequence of using peer-to-peer transmission +to receive a copy likewise does not require acceptance. However, +nothing other than this License grants you permission to propagate or +modify any covered work. These actions infringe copyright if you do +not accept this License. Therefore, by modifying or propagating a +covered work, you indicate your acceptance of this License to do so. + +#### 10. Automatic Licensing of Downstream Recipients. + +Each time you convey a covered work, the recipient automatically +receives a license from the original licensors, to run, modify and +propagate that work, subject to this License. You are not responsible +for enforcing compliance by third parties with this License. + +An "entity transaction" is a transaction transferring control of an +organization, or substantially all assets of one, or subdividing an +organization, or merging organizations. If propagation of a covered +work results from an entity transaction, each party to that +transaction who receives a copy of the work also receives whatever +licenses to the work the party's predecessor in interest had or could +give under the previous paragraph, plus a right to possession of the +Corresponding Source of the work from the predecessor in interest, if +the predecessor has it or can get it with reasonable efforts. + +You may not impose any further restrictions on the exercise of the +rights granted or affirmed under this License. For example, you may +not impose a license fee, royalty, or other charge for exercise of +rights granted under this License, and you may not initiate litigation +(including a cross-claim or counterclaim in a lawsuit) alleging that +any patent claim is infringed by making, using, selling, offering for +sale, or importing the Program or any portion of it. + +#### 11. Patents. + +A "contributor" is a copyright holder who authorizes use under this +License of the Program or a work on which the Program is based. The +work thus licensed is called the contributor's "contributor version". + +A contributor's "essential patent claims" are all patent claims owned +or controlled by the contributor, whether already acquired or +hereafter acquired, that would be infringed by some manner, permitted +by this License, of making, using, or selling its contributor version, +but do not include claims that would be infringed only as a +consequence of further modification of the contributor version. For +purposes of this definition, "control" includes the right to grant +patent sublicenses in a manner consistent with the requirements of +this License. + +Each contributor grants you a non-exclusive, worldwide, royalty-free +patent license under the contributor's essential patent claims, to +make, use, sell, offer for sale, import and otherwise run, modify and +propagate the contents of its contributor version. + +In the following three paragraphs, a "patent license" is any express +agreement or commitment, however denominated, not to enforce a patent +(such as an express permission to practice a patent or covenant not to +sue for patent infringement). To "grant" such a patent license to a +party means to make such an agreement or commitment not to enforce a +patent against the party. + +If you convey a covered work, knowingly relying on a patent license, +and the Corresponding Source of the work is not available for anyone +to copy, free of charge and under the terms of this License, through a +publicly available network server or other readily accessible means, +then you must either (1) cause the Corresponding Source to be so +available, or (2) arrange to deprive yourself of the benefit of the +patent license for this particular work, or (3) arrange, in a manner +consistent with the requirements of this License, to extend the patent +license to downstream recipients. "Knowingly relying" means you have +actual knowledge that, but for the patent license, your conveying the +covered work in a country, or your recipient's use of the covered work +in a country, would infringe one or more identifiable patents in that +country that you have reason to believe are valid. + +If, pursuant to or in connection with a single transaction or +arrangement, you convey, or propagate by procuring conveyance of, a +covered work, and grant a patent license to some of the parties +receiving the covered work authorizing them to use, propagate, modify +or convey a specific copy of the covered work, then the patent license +you grant is automatically extended to all recipients of the covered +work and works based on it. + +A patent license is "discriminatory" if it does not include within the +scope of its coverage, prohibits the exercise of, or is conditioned on +the non-exercise of one or more of the rights that are specifically +granted under this License. You may not convey a covered work if you +are a party to an arrangement with a third party that is in the +business of distributing software, under which you make payment to the +third party based on the extent of your activity of conveying the +work, and under which the third party grants, to any of the parties +who would receive the covered work from you, a discriminatory patent +license (a) in connection with copies of the covered work conveyed by +you (or copies made from those copies), or (b) primarily for and in +connection with specific products or compilations that contain the +covered work, unless you entered into that arrangement, or that patent +license was granted, prior to 28 March 2007. + +Nothing in this License shall be construed as excluding or limiting +any implied license or other defenses to infringement that may +otherwise be available to you under applicable patent law. + +#### 12. No Surrender of Others' Freedom. + +If conditions are imposed on you (whether by court order, agreement or +otherwise) that contradict the conditions of this License, they do not +excuse you from the conditions of this License. If you cannot convey a +covered work so as to satisfy simultaneously your obligations under +this License and any other pertinent obligations, then as a +consequence you may not convey it at all. For example, if you agree to +terms that obligate you to collect a royalty for further conveying +from those to whom you convey the Program, the only way you could +satisfy both those terms and this License would be to refrain entirely +from conveying the Program. + +#### 13. Remote Network Interaction; Use with the GNU General Public License. + +Notwithstanding any other provision of this License, if you modify the +Program, your modified version must prominently offer all users +interacting with it remotely through a computer network (if your +version supports such interaction) an opportunity to receive the +Corresponding Source of your version by providing access to the +Corresponding Source from a network server at no charge, through some +standard or customary means of facilitating copying of software. This +Corresponding Source shall include the Corresponding Source for any +work covered by version 3 of the GNU General Public License that is +incorporated pursuant to the following paragraph. + +Notwithstanding any other provision of this License, you have +permission to link or combine any covered work with a work licensed +under version 3 of the GNU General Public License into a single +combined work, and to convey the resulting work. The terms of this +License will continue to apply to the part which is the covered work, +but the work with which it is combined will remain governed by version +3 of the GNU General Public License. + +#### 14. Revised Versions of this License. + +The Free Software Foundation may publish revised and/or new versions +of the GNU Affero General Public License from time to time. Such new +versions will be similar in spirit to the present version, but may +differ in detail to address new problems or concerns. + +Each version is given a distinguishing version number. If the Program +specifies that a certain numbered version of the GNU Affero General +Public License "or any later version" applies to it, you have the +option of following the terms and conditions either of that numbered +version or of any later version published by the Free Software +Foundation. If the Program does not specify a version number of the +GNU Affero General Public License, you may choose any version ever +published by the Free Software Foundation. + +If the Program specifies that a proxy can decide which future versions +of the GNU Affero General Public License can be used, that proxy's +public statement of acceptance of a version permanently authorizes you +to choose that version for the Program. + +Later license versions may give you additional or different +permissions. However, no additional obligations are imposed on any +author or copyright holder as a result of your choosing to follow a +later version. + +#### 15. Disclaimer of Warranty. + +THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY +APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT +HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT +WARRANTY OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT +LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +A PARTICULAR PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND +PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE PROGRAM PROVE +DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, REPAIR OR +CORRECTION. + +#### 16. Limitation of Liability. + +IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING +WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR +CONVEYS THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, +INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES +ARISING OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT +NOT LIMITED TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR +LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM +TO OPERATE WITH ANY OTHER PROGRAMS), EVEN IF SUCH HOLDER OR OTHER +PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES. + +#### 17. Interpretation of Sections 15 and 16. + +If the disclaimer of warranty and limitation of liability provided +above cannot be given local legal effect according to their terms, +reviewing courts shall apply local law that most closely approximates +an absolute waiver of all civil liability in connection with the +Program, unless a warranty or assumption of liability accompanies a +copy of the Program in return for a fee. + +END OF TERMS AND CONDITIONS + +### How to Apply These Terms to Your New Programs + +If you develop a new program, and you want it to be of the greatest +possible use to the public, the best way to achieve this is to make it +free software which everyone can redistribute and change under these +terms. + +To do so, attach the following notices to the program. It is safest to +attach them to the start of each source file to most effectively state +the exclusion of warranty; and each file should have at least the +"copyright" line and a pointer to where the full notice is found. + + + Copyright (C) + + 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 . + +Also add information on how to contact you by electronic and paper +mail. + +If your software can interact with users remotely through a computer +network, you should also make sure that it provides a way for users to +get its source. For example, if your program is a web application, its +interface could display a "Source" link that leads users to an archive +of the code. There are many ways you could offer source, and different +solutions will be better for different programs; see section 13 for +the specific requirements. + +You should also get your employer (if you work as a programmer) or +school, if any, to sign a "copyright disclaimer" for the program, if +necessary. For more information on this, and how to apply and follow +the GNU AGPL, see . diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..121a3a8 --- /dev/null +++ b/Makefile @@ -0,0 +1,53 @@ +default: frontend ## Debug build + cargo build + +clean: ## Clean all build artifacts and dependencies + @cargo clean + @yarn cache clean + @-rm -rf browser/pkg + @-rm ./src/cache_buster_data.json + @-rm -rf ./static/cache/bundle + @-rm -rf ./assets + +coverage: migrate ## Generate HTML code coverage + cargo tarpaulin -t 1200 --out Html + +dev-env: ## Download development dependencies + cargo fetch + yarn install + +doc: ## Prepare documentation + cargo doc --no-deps --workspace --all-features + +docker: ## Build docker images + docker build -t mcapthca/survey:master -t mcapthca/survey:latest . + +docker-publish: docker ## Build and publish docker images + docker push mcapthca/survey:master + docker push mcapthca/survey:latest + +frontend: ## Build frontend assets + @yarn install + @-rm -rf ./static/cache/bundle/ + @-mkdir ./static/cache/bundle/css/ + @yarn run dart-sass -s compressed templates/main.scss ./static/cache/bundle/css/main.css + +migrate: ## Run database migrations + cargo run --bin tests-migrate + +release: frontend ## Release build + cargo build --release + +run: default ## Run debug build + cargo run + +test: frontend ## Run tests + echo 'static/' && tree static || true + echo 'tree/' && tree assets || true + cargo test --all-features --no-fail-fast + +xml-test-coverage: migrate ## Generate cobertura.xml test coverage + 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/build.rs b/build.rs new file mode 100644 index 0000000..af9b4f7 --- /dev/null +++ b/build.rs @@ -0,0 +1,69 @@ +/* + * 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; + +use cache_buster::{BusterBuilder, NoHashCategory}; +use sqlx::types::time::OffsetDateTime; + +fn main() { + // note: add error checking yourself. + let output = Command::new("git") + .args(&["rev-parse", "HEAD"]) + .output() + .unwrap(); + let git_hash = String::from_utf8(output.stdout).unwrap(); + println!("cargo:rustc-env=GIT_HASH={}", git_hash); + + // let yml = include_str!("./openapi.yaml"); + // let api_json: serde_json::Value = serde_yaml::from_str(yml).unwrap(); + // println!( + // "cargo:rustc-env=OPEN_API_DOCS={}", + // serde_json::to_string(&api_json).unwrap() + // ); + + let now = OffsetDateTime::now_utc().format("%y-%m-%d"); + println!("cargo:rustc-env=COMPILED_DATE={}", &now); + + cache_bust(); +} + +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!("cargo:rerun-if-changed=static/cache"); + let no_hash = vec![NoHashCategory::FileExtentions(vec!["wasm"])]; + + let config = BusterBuilder::default() + .source("./static/cache/") + .result("./assets") + .copy(true) + .no_hash(no_hash) + .follow_links(true) + .build() + .unwrap(); + + config.process().unwrap(); +} diff --git a/config/default.toml b/config/default.toml new file mode 100644 index 0000000..048c7a9 --- /dev/null +++ b/config/default.toml @@ -0,0 +1,33 @@ +debug = true +allow_registration = true +source_code = "https://github.com/mcaptcha/survey" +password = "password" + +[server] +# Please set a unique value, your kaizen instance's security depends on this being +# unique +cookie_secret = "8ce364dab188452ffa76c3e1869be5d40dcb9db4826b7b78a3e6ce1a8ca19d32" +# The port at which you want authentication to listen to +# takes a number, choose from 1000-10000 if you dont know what you are doing +port = 7000 +#IP address. Enter 0.0.0.0 to listen on all availale addresses +ip= "0.0.0.0" +# enter your hostname, eg: example.com +domain = "localhost" +allow_registration = true +proxy_has_tls = false + +[database] +# This section deals with the database location and how to access it +# Please note that at the moment, we have support for only postgresqa. +# Example, if you are Batman, your config would be: +# hostname = "batcave.org" +# port = "5432" +# username = "batman" +# password = "somereallycomplicatedBatmanpassword" +hostname = "localhost" +port = "5432" +username = "postgres" +password = "password" +name = "postgres" +pool = 4 diff --git a/migrations/20211001071311_survey_users.sql b/migrations/20211001071311_survey_users.sql new file mode 100644 index 0000000..0bdc223 --- /dev/null +++ b/migrations/20211001071311_survey_users.sql @@ -0,0 +1,5 @@ +CREATE TABLE IF NOT EXISTS survey_users ( + ID UUID PRIMARY KEY NOT NULL UNIQUE, + created_at TIMESTAMPTZ NOT NULL + device VARCHAR(100) NOT NULL, +) diff --git a/package.json b/package.json new file mode 100644 index 0000000..a863f76 --- /dev/null +++ b/package.json @@ -0,0 +1,13 @@ +{ + "name": "mcaptcha-survey", + "main": "index.js", + "version": "1.0.0", + "scripts": { + "prod": "yarn run dart-sass", + "start": "webpack-dev-server --mode development --progress --color", + "test": "jest" + }, + "devDependencies": { + "dart-sass": "^1.25.0" + } +} diff --git a/rustfmt.toml b/rustfmt.toml new file mode 100644 index 0000000..038034f --- /dev/null +++ b/rustfmt.toml @@ -0,0 +1 @@ +max_width = 89 diff --git a/sailfish.yml b/sailfish.yml new file mode 100644 index 0000000..5f64209 --- /dev/null +++ b/sailfish.yml @@ -0,0 +1 @@ +delimiter: "." diff --git a/src/api/mod.rs b/src/api/mod.rs new file mode 100644 index 0000000..cce780a --- /dev/null +++ b/src/api/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 . + */ + +pub mod v1; diff --git a/src/api/v1/auth.rs b/src/api/v1/auth.rs new file mode 100644 index 0000000..d11af04 --- /dev/null +++ b/src/api/v1/auth.rs @@ -0,0 +1,92 @@ +/* + * 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::http::header; +use actix_web::{web, HttpResponse, Responder}; +use serde::{Deserialize, Serialize}; + +use super::get_random; +use super::get_uuid; +use crate::errors::*; +use crate::AppData; + +pub mod routes { + pub struct Auth { + pub register: &'static str, + } + + impl Auth { + pub const fn new() -> Auth { + let register = "/api/v1/signup"; + Auth { register } + } + } +} + +pub mod runners { + // use std::borrow::Cow; + + use super::*; + + pub async fn register_runner() -> ServiceResult { + let mut uuid; + + loop { + uuid = get_uuid(); + + // let res= sqlx::query!( + // "INSERT INTO + // kaizen_feedbacks (helpful , description, uuid, campaign_id, time, page_url) + // VALUES ($1, $2, $3, $4, $5, + // (SELECT ID from kaizen_campaign_pages WHERE page_url = $6))", + // &payload.helpful, + // &payload.description, + // &uuid, + // &campaign_id, + // &now, + // &payload.page_url, + // ) + // .execute(&data.db) + // .await; + // + // if res.is_ok() { + // break; + // } else if let Err(sqlx::Error::Database(err)) = res { + // if err.code() == Some(Cow::from("23505")) + // && err.message().contains("kaizen_campaign_uuid_key") + // { + // continue; + // } else { + // return Err(sqlx::Error::Database(err).into()); + // } + // } + // } + } + Ok(uuid) + } +} + +pub fn services(cfg: &mut web::ServiceConfig) { + cfg.service(register); +} +#[my_codegen::post(path = "crate::V1_API_ROUTES.auth.register")] +async fn register(data: AppData, id: Identity) -> ServiceResult { + let uuid = runners::register_runner().await?; + id.remember(uuid.to_string()); + Ok(HttpResponse::Ok()) +} diff --git a/src/api/v1/meta.rs b/src/api/v1/meta.rs new file mode 100644 index 0000000..fd90971 --- /dev/null +++ b/src/api/v1/meta.rs @@ -0,0 +1,124 @@ +/* +* 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::{web, HttpResponse, Responder}; +use derive_builder::Builder; +use serde::{Deserialize, Serialize}; + +use crate::AppData; +use crate::{GIT_COMMIT_HASH, VERSION}; + +#[derive(Clone, Debug, Deserialize, Builder, Serialize)] +pub struct BuildDetails { + pub version: &'static str, + pub git_commit_hash: &'static 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", + } + } + } +} + +/// emmits build details of the bninary +#[my_codegen::get(path = "crate::V1_API_ROUTES.meta.build_details")] +async fn build_details() -> impl Responder { + let build = BuildDetails { + version: VERSION, + git_commit_hash: GIT_COMMIT_HASH, + }; + HttpResponse::Ok().json(build) +} + +#[derive(Clone, Debug, Deserialize, Builder, Serialize)] +/// Health check return datatype +pub struct Health { + db: bool, +} + +/// checks all components of the system +#[my_codegen::get(path = "crate::V1_API_ROUTES.meta.health")] +async fn health(data: AppData) -> impl Responder { + use sqlx::Connection; + + let mut resp_builder = HealthBuilder::default(); + resp_builder.db(false); + + if let Ok(mut con) = data.db.acquire().await { + if con.ping().await.is_ok() { + resp_builder.db(true); + } + }; + HttpResponse::Ok().json(resp_builder.build().unwrap()) +} + +pub fn services(cfg: &mut web::ServiceConfig) { + cfg.service(build_details); + cfg.service(health); +} + +#[cfg(test)] +mod tests { + use actix_web::{http::StatusCode, test, App}; + + use super::*; + use crate::api::v1::services; + use crate::*; + + #[actix_rt::test] + async fn build_details_works() { + let app = test::init_service(App::new().configure(services)).await; + + let resp = test::call_service( + &app, + test::TestRequest::get() + .uri(V1_API_ROUTES.meta.build_details) + .to_request(), + ) + .await; + assert_eq!(resp.status(), StatusCode::OK); + } + + // #[actix_rt::test] + // async fn health_works() { + // println!("{}", V1_API_ROUTES.meta.health); + // let data = Data::new().await; + // let app = get_app!(data).await; + // + // let resp = test::call_service( + // &app, + // test::TestRequest::get() + // .uri(V1_API_ROUTES.meta.health) + // .to_request(), + // ) + // .await; + // assert_eq!(resp.status(), StatusCode::OK); + // + // let health_resp: 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..49727a0 --- /dev/null +++ b/src/api/v1/mod.rs @@ -0,0 +1,45 @@ +/* + * 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::web::ServiceConfig; +use uuid::Uuid; + +pub mod auth; +mod meta; +pub mod routes; +pub use routes::ROUTES; + +pub fn services(cfg: &mut ServiceConfig) { + meta::services(cfg); + auth::services(cfg); +} + +pub 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 fn get_uuid() -> Uuid { + Uuid::new_v4() +} diff --git a/src/api/v1/routes.rs b/src/api/v1/routes.rs new file mode 100644 index 0000000..6cd6471 --- /dev/null +++ b/src/api/v1/routes.rs @@ -0,0 +1,34 @@ +/* +* 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 super::auth::routes::Auth; +use super::meta::routes::Meta; + +pub const ROUTES: Routes = Routes::new(); + +pub struct Routes { + pub auth: Auth, + pub meta: Meta, +} + +impl Routes { + const fn new() -> Routes { + Routes { + auth: Auth::new(), + meta: Meta::new(), + } + } +} diff --git a/src/data.rs b/src/data.rs new file mode 100644 index 0000000..a86d86e --- /dev/null +++ b/src/data.rs @@ -0,0 +1,45 @@ +/* + * 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 . + */ +//! App data: database connections, etc. +use std::sync::Arc; + +use sqlx::postgres::PgPoolOptions; +use sqlx::PgPool; + +use crate::SETTINGS; + +/// App data +pub struct Data { + /// databse pool + pub db: PgPool, +} + +impl Data { + #[cfg(not(tarpaulin_include))] + /// create new instance of app data + pub async fn new() -> Arc { + let db = PgPoolOptions::new() + .max_connections(SETTINGS.database.pool) + .connect(&SETTINGS.database.url) + .await + .expect("Unable to form database pool"); + + let data = Data { db }; + + Arc::new(data) + } +} diff --git a/src/errors.rs b/src/errors.rs new file mode 100644 index 0000000..38ed977 --- /dev/null +++ b/src/errors.rs @@ -0,0 +1,201 @@ +/* +* 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::convert::From; + +use actix::MailboxError; +use actix_web::{ + error::ResponseError, + http::{header, StatusCode}, + HttpResponse, HttpResponseBuilder, +}; +use derive_more::{Display, Error}; +use libmcaptcha::errors::CaptchaError; +use serde::{Deserialize, Serialize}; +use tokio::sync::oneshot::error::RecvError; +use url::ParseError; +use validator::ValidationErrors; + +#[derive(Debug, Display, PartialEq, Error)] +#[cfg(not(tarpaulin_include))] +pub enum ServiceError { + #[display(fmt = "internal server error")] + InternalServerError, + + /// when the a username is already taken + #[display(fmt = "Username not available")] + UsernameTaken, + + /// email is already taken + #[display(fmt = "Email not available")] + EmailTaken, + + /// when the a token name is already taken + /// token not found + #[display(fmt = "Token not found. Is token registered?")] + TokenNotFound, + + #[display(fmt = "{}", _0)] + CaptchaError(CaptchaError), +} + +#[derive(Serialize, Deserialize)] +#[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(), + ) + .into() + } + + #[cfg(not(tarpaulin_include))] + fn status_code(&self) -> StatusCode { + match self { + ServiceError::InternalServerError => StatusCode::INTERNAL_SERVER_ERROR, + + ServiceError::UsernameTaken => StatusCode::BAD_REQUEST, + ServiceError::EmailTaken => StatusCode::BAD_REQUEST, + + ServiceError::TokenNotFound => StatusCode::NOT_FOUND, + ServiceError::CaptchaError(e) => { + log::error!("{}", e); + match e { + CaptchaError::MailboxError => StatusCode::INTERNAL_SERVER_ERROR, + _ => StatusCode::BAD_REQUEST, + } + } + } + } +} + +#[cfg(not(tarpaulin_include))] +impl From for ServiceError { + fn from(e: CaptchaError) -> ServiceError { + ServiceError::CaptchaError(e) + } +} + +#[cfg(not(tarpaulin_include))] +impl From for ServiceError { + #[cfg(not(tarpaulin_include))] + fn from(e: sqlx::Error) -> Self { + // use sqlx::error::Error; + // use std::borrow::Cow; + + ServiceError::InternalServerError + } +} + +#[cfg(not(tarpaulin_include))] +impl From for ServiceError { + #[cfg(not(tarpaulin_include))] + fn from(e: RecvError) -> Self { + log::error!("{:?}", e); + ServiceError::InternalServerError + } +} + +#[cfg(not(tarpaulin_include))] +impl From for ServiceError { + #[cfg(not(tarpaulin_include))] + fn from(e: MailboxError) -> Self { + log::error!("{:?}", e); + ServiceError::InternalServerError + } +} + +#[cfg(not(tarpaulin_include))] +pub type ServiceResult = std::result::Result; + +//#[derive(Debug, Display, PartialEq, Error)] +//#[cfg(not(tarpaulin_include))] +//pub enum PageError { +// #[display(fmt = "Something weng wrong: Internal server error")] +// InternalServerError, +// +// #[display(fmt = "{}", _0)] +// ServiceError(ServiceError), +//} +// +//#[cfg(not(tarpaulin_include))] +//impl From for PageError { +// #[cfg(not(tarpaulin_include))] +// fn from(_: sqlx::Error) -> Self { +// PageError::InternalServerError +// } +//} +// +//#[cfg(not(tarpaulin_include))] +//impl From for PageError { +// #[cfg(not(tarpaulin_include))] +// fn from(e: ServiceError) -> Self { +// PageError::ServiceError(e) +// } +//} +// +//impl ResponseError for PageError { +// fn error_response(&self) -> HttpResponse { +// use crate::PAGES; +// match self.status_code() { +// StatusCode::INTERNAL_SERVER_ERROR => HttpResponse::Found() +// .append_header((header::LOCATION, PAGES.errors.internal_server_error)) +// .finish(), +// _ => HttpResponse::Found() +// .append_header((header::LOCATION, PAGES.errors.unknown_error)) +// .finish(), +// } +// } +// +// #[cfg(not(tarpaulin_include))] +// fn status_code(&self) -> StatusCode { +// match self { +// PageError::InternalServerError => StatusCode::INTERNAL_SERVER_ERROR, +// PageError::ServiceError(e) => e.status_code(), +// } +// } +//} +// +//#[cfg(not(tarpaulin_include))] +//pub type PageResult = std::result::Result; +// +//#[cfg(test)] +//mod tests { +// use super::*; +// use crate::PAGES; +// +// #[test] +// fn error_works() { +// let resp: HttpResponse = PageError::InternalServerError.error_response(); +// assert_eq!(resp.status(), StatusCode::FOUND); +// let headers = resp.headers(); +// assert_eq!( +// headers.get(header::LOCATION).unwrap(), +// PAGES.errors.internal_server_error +// ); +// } +//} diff --git a/src/main.rs b/src/main.rs new file mode 100644 index 0000000..00d11b5 --- /dev/null +++ b/src/main.rs @@ -0,0 +1,143 @@ +/* + * 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::env; +use std::sync::Arc; + +use actix_identity::{CookieIdentityPolicy, IdentityService}; +use actix_web::{ + error::InternalError, http::StatusCode, middleware as actix_middleware, + web::JsonConfig, App, HttpServer, +}; +use lazy_static::lazy_static; +use log::info; + +mod api; +mod data; +mod errors; +mod middleware; +//mod pages; +mod settings; +//mod static_assets; +//#[cfg(test)] +//#[macro_use] +//mod tests; + +pub use crate::data::Data; +pub use api::v1::ROUTES as V1_API_ROUTES; +pub use middleware::auth::CheckLogin; +//pub use pages::routes::ROUTES as PAGES; +pub use settings::Settings; +//pub use static_assets::static_files::assets; +// +//use static_assets::FileMap; + +lazy_static! { + pub static ref SETTINGS: Settings = Settings::new().unwrap(); +// pub static ref FILES: FileMap = FileMap::new(); +// +// pub static ref CSS: &'static str = +// FILES.get("./static/cache/bundle/css/main.css").unwrap(); + /// points to source files matching build commit + pub static ref SOURCE_FILES_OF_INSTANCE: String = { + let mut url = SETTINGS.source_code.clone(); + if !url.ends_with('/') { + url.push('/'); + } + let mut base = url::Url::parse(&url).unwrap(); + base = base.join("tree/").unwrap(); + base = base.join(GIT_COMMIT_HASH).unwrap(); + base.into() + }; +} + +pub const CACHE_AGE: u32 = 604800; + +pub const COMPILED_DATE: &str = env!("COMPILED_DATE"); +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 AppData = actix_web::web::Data>; + +#[cfg(not(tarpaulin_include))] +#[actix_web::main] +async fn main() -> std::io::Result<()> { + env::set_var("RUST_LOG", "info"); + + pretty_env_logger::init(); + + info!( + "{}: {}.\nFor more information, see: {}\nBuild info:\nVersion: {} commit: {}", + PKG_NAME, PKG_DESCRIPTION, PKG_HOMEPAGE, VERSION, GIT_COMMIT_HASH + ); + + let data = Data::new().await; + sqlx::migrate!("./migrations/").run(&data.db).await.unwrap(); + let data = actix_web::web::Data::new(data); + + println!("Starting server on: http://{}", SETTINGS.server.get_ip()); + + HttpServer::new(move || { + App::new() + .wrap(actix_middleware::Logger::default()) + .wrap(actix_middleware::Compress::default()) + .app_data(get_json_err()) + .wrap( + actix_middleware::DefaultHeaders::new() + .header("Permissions-Policy", "interest-cohort=()"), + ) + .wrap(get_identity_service()) + .wrap(actix_middleware::NormalizePath::new( + actix_middleware::TrailingSlash::Trim, + )) + .configure(services) + .app_data(data.clone()) + }) + .bind(SETTINGS.server.get_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() -> IdentityService { + let cookie_secret = &SETTINGS.server.cookie_secret; + IdentityService::new( + CookieIdentityPolicy::new(cookie_secret.as_bytes()) + .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) { + //pages::services(cfg); + api::v1::services(cfg); + // static_assets::services(cfg); +} diff --git a/src/middleware/auth.rs b/src/middleware/auth.rs new file mode 100644 index 0000000..ef18902 --- /dev/null +++ b/src/middleware/auth.rs @@ -0,0 +1,81 @@ +/* + * 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 . + */ +#![allow(clippy::type_complexity)] + +use actix_http::body::AnyBody; +use actix_identity::Identity; +use actix_service::{Service, Transform}; +use actix_web::dev::{ServiceRequest, ServiceResponse}; +use actix_web::{http, Error, FromRequest, HttpResponse}; + +use futures::future::{ok, Either, Ready}; + +pub const AUTH: &str = "/login"; //crate::PAGES.auth.login; + +pub struct CheckLogin; + +impl Transform for CheckLogin +where + S: Service, Error = Error>, + S::Future: 'static, +{ + type Response = ServiceResponse; + type Error = Error; + type Transform = CheckLoginMiddleware; + type InitError = (); + type Future = Ready>; + + fn new_transform(&self, service: S) -> Self::Future { + ok(CheckLoginMiddleware { service }) + } +} +pub struct CheckLoginMiddleware { + service: S, +} + +impl Service for CheckLoginMiddleware +where + S: Service, Error = Error>, + S::Future: 'static, +{ + type Response = ServiceResponse; + type Error = Error; + type Future = Either>>; + + actix_service::forward_ready!(service); + + fn call(&self, req: ServiceRequest) -> Self::Future { + let (r, mut pl) = req.into_parts(); + + // TODO investigate when the bellow statement will + // return error + if let Ok(Some(_)) = Identity::from_request(&r, &mut pl) + .into_inner() + .map(|x| x.identity()) + { + let req = ServiceRequest::from_parts(r, pl); + Either::Left(self.service.call(req)) + } else { + let req = ServiceRequest::from_parts(r, pl); //.ok().unwrap(); + Either::Right(ok(req.into_response( + HttpResponse::Found() + .insert_header((http::header::LOCATION, AUTH)) + .finish(), + ))) + } + } +} diff --git a/src/middleware/mod.rs b/src/middleware/mod.rs new file mode 100644 index 0000000..1f5d11b --- /dev/null +++ b/src/middleware/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 . + */ + +pub mod auth; diff --git a/src/pages/auth/join.rs b/src/pages/auth/join.rs new file mode 100644 index 0000000..19ee487 --- /dev/null +++ b/src/pages/auth/join.rs @@ -0,0 +1,158 @@ +/* + * 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::HttpResponseBuilder; +use actix_web::{error::ResponseError, http::header, web, HttpResponse, Responder}; +use lazy_static::lazy_static; +use sailfish::TemplateOnce; + +use crate::api::v1::auth::runners; +use crate::errors::*; +use crate::pages::errors::ErrorPage; +use crate::AppData; +use crate::PAGES; + +#[derive(Clone, TemplateOnce)] +#[template(path = "auth/join/index.html")] +struct IndexPage<'a> { + error: Option>, +} + +const PAGE: &str = "Join"; + +impl<'a> Default for IndexPage<'a> { + fn default() -> Self { + IndexPage { error: None } + } +} + +impl<'a> IndexPage<'a> { + pub fn new(title: &'a str, message: &'a str) -> Self { + Self { + error: Some(ErrorPage::new(title, message)), + } + } +} + +lazy_static! { + static ref INDEX: String = IndexPage::default().render_once().unwrap(); +} + +#[my_codegen::get(path = "crate::PAGES.auth.join")] +pub async fn join() -> impl Responder { + HttpResponse::Ok() + .content_type("text/html; charset=utf-8") + .body(&*INDEX) +} + +#[my_codegen::post(path = "PAGES.auth.join")] +pub async fn join_submit( + payload: web::Form, + data: AppData, +) -> PageResult { + let mut payload = payload.into_inner(); + if payload.email.is_some() && payload.email.as_ref().unwrap().is_empty() { + payload.email = None; + } + + match runners::register_runner(&payload, &data).await { + Ok(()) => Ok(HttpResponse::Found() + .insert_header((header::LOCATION, PAGES.auth.login)) + .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::account::{ + username::runners::username_exists, AccountCheckPayload, + }; + use crate::api::v1::auth::runners::Register; + use crate::data::Data; + use crate::tests::*; + use crate::*; + use actix_web::http::StatusCode; + + #[actix_rt::test] + async fn auth_join_form_works() { + let data = Data::new().await; + const NAME: &str = "testuserformjoin"; + const NAME2: &str = "testuserformjoin2"; + const EMAIL: &str = "testuserformjoin@a.com"; + const PASSWORD: &str = "longpassword"; + + let app = get_app!(data).await; + + delete_user(NAME, &data).await; + + // 1. Register with email == None + let mut msg = Register { + username: NAME.into(), + password: PASSWORD.into(), + confirm_password: PASSWORD.into(), + email: Some(EMAIL.into()), + }; + + let resp = test::call_service( + &app, + post_request!(&msg, PAGES.auth.join, FORM).to_request(), + ) + .await; + assert_eq!(resp.status(), StatusCode::FOUND); + let headers = resp.headers(); + assert_eq!(headers.get(header::LOCATION).unwrap(), PAGES.auth.login,); + + let account_check = AccountCheckPayload { val: NAME.into() }; + assert!( + username_exists(&account_check, &AppData::new(data.clone())) + .await + .unwrap() + .exists + ); + + msg.email = None; + let resp = test::call_service( + &app, + post_request!(&msg, PAGES.auth.join, FORM).to_request(), + ) + .await; + assert_eq!(resp.status(), StatusCode::BAD_REQUEST); + + msg.email = Some(EMAIL.into()); + msg.username = NAME2.into(); + let resp = test::call_service( + &app, + post_request!(&msg, PAGES.auth.join, FORM).to_request(), + ) + .await; + assert_eq!(resp.status(), StatusCode::BAD_REQUEST); + } +} diff --git a/src/pages/auth/login.rs b/src/pages/auth/login.rs new file mode 100644 index 0000000..08be460 --- /dev/null +++ b/src/pages/auth/login.rs @@ -0,0 +1,167 @@ +/* + * 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::HttpResponseBuilder; +use actix_web::{error::ResponseError, http::header, web, HttpResponse, Responder}; +use lazy_static::lazy_static; +use my_codegen::{get, post}; +use sailfish::TemplateOnce; + +use crate::api::v1::auth::runners; +use crate::errors::*; +use crate::pages::errors::ErrorPage; +use crate::AppData; +use crate::PAGES; + +#[derive(Clone, TemplateOnce)] +#[template(path = "auth/login/index.html")] +struct IndexPage<'a> { + error: Option>, +} + +const PAGE: &str = "Login"; + +impl<'a> Default for IndexPage<'a> { + fn default() -> Self { + IndexPage { error: None } + } +} + +impl<'a> IndexPage<'a> { + pub fn new(title: &'a str, message: &'a str) -> Self { + Self { + error: Some(ErrorPage::new(title, message)), + } + } +} + +lazy_static! { + static ref INDEX: String = IndexPage::default().render_once().unwrap(); +} + +#[get(path = "PAGES.auth.login")] +pub async fn login() -> impl Responder { + HttpResponse::Ok() + .content_type("text/html; charset=utf-8") + .body(&*INDEX) +} + +#[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/mod.rs b/src/pages/auth/mod.rs new file mode 100644 index 0000000..ae5025b --- /dev/null +++ b/src/pages/auth/mod.rs @@ -0,0 +1,46 @@ +/* + * 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 . + */ +pub mod join; +pub mod login; +pub mod sudo; + +pub fn services(cfg: &mut actix_web::web::ServiceConfig) { + cfg.service(login::login); + cfg.service(login::login_submit); + cfg.service(join::join); + cfg.service(join::join_submit); +} + +pub mod routes { + pub struct Auth { + pub login: &'static str, + pub join: &'static str, + } + impl Auth { + pub const fn new() -> Auth { + Auth { + login: "/login", + join: "/join", + } + } + + pub const fn get_sitemap() -> [&'static str; 2] { + const AUTH: Auth = Auth::new(); + [AUTH.login, AUTH.join] + } + } +} diff --git a/src/pages/auth/sudo.rs b/src/pages/auth/sudo.rs new file mode 100644 index 0000000..73b6d64 --- /dev/null +++ b/src/pages/auth/sudo.rs @@ -0,0 +1,43 @@ +/* + * 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 sailfish::TemplateOnce; + +use crate::pages::errors::ErrorPage; + +#[derive(Clone, TemplateOnce)] +#[template(path = "auth/sudo/index.html")] +pub struct SudoPage<'a> { + url: &'a str, + title: &'a str, + error: Option>, +} + +pub const PAGE: &str = "Confirm Access"; + +impl<'a> SudoPage<'a> { + pub fn new(url: &'a str, title: &'a str) -> Self { + Self { + url, + title, + error: None, + } + } + + pub fn set_err(&mut self, err_title: &'a str, message: &'a str) { + self.error = Some(ErrorPage::new(err_title, message)); + } +} diff --git a/src/pages/errors.rs b/src/pages/errors.rs new file mode 100644 index 0000000..657c55f --- /dev/null +++ b/src/pages/errors.rs @@ -0,0 +1,120 @@ +/* + * 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::{web, HttpResponse, Responder}; +use lazy_static::lazy_static; +use sailfish::TemplateOnce; + +use crate::errors::PageError; + +#[derive(Clone, TemplateOnce)] +#[template(path = "errors/index.html")] +pub struct ErrorPage<'a> { + pub title: &'a str, + pub message: &'a str, +} + +const PAGE: &str = "Error"; + +impl<'a> ErrorPage<'a> { + pub fn new(title: &'a str, message: &'a str) -> Self { + ErrorPage { title, message } + } +} + +lazy_static! { + static ref INTERNAL_SERVER_ERROR_BODY: String = ErrorPage::new( + "Internal Server Error", + &format!("{}", PageError::InternalServerError), + ) + .render_once() + .unwrap(); + static ref UNKNOWN_ERROR_BODY: String = ErrorPage::new( + "Something went wrong", + &format!("{}", PageError::InternalServerError), + ) + .render_once() + .unwrap(); +} + +const ERROR_ROUTE: &str = "/error/{id}"; + +#[my_codegen::get(path = "ERROR_ROUTE")] +async fn error(path: web::Path) -> impl Responder { + let resp = match path.into_inner() { + 500 => HttpResponse::InternalServerError() + .content_type("text/html; charset=utf-8") + .body(&*INTERNAL_SERVER_ERROR_BODY), + + _ => HttpResponse::InternalServerError() + .content_type("text/html; charset=utf-8") + .body(&*UNKNOWN_ERROR_BODY), + }; + + resp +} + +pub fn services(cfg: &mut web::ServiceConfig) { + cfg.service(error); +} + +pub mod routes { + pub struct Errors { + pub internal_server_error: &'static str, + pub unknown_error: &'static str, + } + + impl Errors { + pub const fn new() -> Self { + Errors { + internal_server_error: "/error/500", + unknown_error: "/error/007", + } + } + } +} + +#[cfg(test)] +mod tests { + use actix_web::{http::StatusCode, test, App}; + + use super::*; + use crate::PAGES; + + #[actix_rt::test] + async fn error_pages_work() { + let app = test::init_service(App::new().configure(services)).await; + + let resp = test::call_service( + &app, + test::TestRequest::get() + .uri(PAGES.errors.internal_server_error) + .to_request(), + ) + .await; + assert_eq!(resp.status(), StatusCode::INTERNAL_SERVER_ERROR); + + let resp = test::call_service( + &app, + test::TestRequest::get() + .uri(PAGES.errors.unknown_error) + .to_request(), + ) + .await; + assert_eq!(resp.status(), StatusCode::INTERNAL_SERVER_ERROR); + } +} diff --git a/src/pages/mod.rs b/src/pages/mod.rs new file mode 100644 index 0000000..276c146 --- /dev/null +++ b/src/pages/mod.rs @@ -0,0 +1,116 @@ +/* + * 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::web::ServiceConfig; + +pub mod auth; +pub mod errors; +mod panel; +pub mod routes; +//mod sitemap; + +pub const NAME: &str = "Kaizen"; + +pub fn services(cfg: &mut ServiceConfig) { + auth::services(cfg); + panel::services(cfg); + errors::services(cfg); +} + +#[cfg(not(tarpaulin_include))] +#[cfg(test)] +mod tests { + use actix_web::http::{header, StatusCode}; + use actix_web::test; + + use crate::tests::*; + use crate::*; + + #[actix_rt::test] + async fn protected_pages_templates_work() { + const NAME: &str = "templateuser"; + const PASSWORD: &str = "longpassword"; + const EMAIL: &str = "templateuser@a.com"; + const CAMPAIGN_NAME: &str = "delcappageusercamaping"; + + let data = Data::new().await; + { + delete_user(NAME, &data).await; + } + + let (_, _, signin_resp) = register_and_signin(NAME, EMAIL, PASSWORD).await; + let cookies = get_cookie!(signin_resp); + + let campaign = + create_new_campaign(CAMPAIGN_NAME, data.clone(), cookies.clone()).await; + + let app = get_app!(data).await; + + let urls = vec![ + PAGES.home.into(), + PAGES.panel.campaigns.home.into(), + PAGES.panel.campaigns.new.into(), + PAGES.panel.campaigns.get_feedback_route(&campaign.uuid), + PAGES.panel.campaigns.get_delete_route(&campaign.uuid), + ]; + + for url in urls.iter() { + let resp = + test::call_service(&app, test::TestRequest::get().uri(url).to_request()) + .await; + if resp.status() != StatusCode::FOUND { + println!("Probably error url: {}", url); + } + assert_eq!(resp.status(), StatusCode::FOUND); + + let authenticated_resp = test::call_service( + &app, + test::TestRequest::get() + .uri(url) + .cookie(cookies.clone()) + .to_request(), + ) + .await; + + if url == &PAGES.home { + assert_eq!(authenticated_resp.status(), StatusCode::FOUND); + let headers = authenticated_resp.headers(); + assert_eq!( + headers.get(header::LOCATION).unwrap(), + PAGES.panel.campaigns.home + ); + } else { + assert_eq!(authenticated_resp.status(), StatusCode::OK); + } + } + + delete_user(NAME, &data).await; + } + + #[actix_rt::test] + async fn public_pages_tempaltes_work() { + let app = test::init_service(App::new().configure(crate::pages::services)).await; + let urls = vec![PAGES.auth.login, PAGES.auth.join]; //, PAGES.sitemap]; + + for url in urls.iter() { + let resp = + test::call_service(&app, test::TestRequest::get().uri(url).to_request()) + .await; + + assert_eq!(resp.status(), StatusCode::OK); + } + } +} diff --git a/src/pages/panel/campaigns/delete.rs b/src/pages/panel/campaigns/delete.rs new file mode 100644 index 0000000..d7199c9 --- /dev/null +++ b/src/pages/panel/campaigns/delete.rs @@ -0,0 +1,196 @@ +/* + * 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::HttpResponseBuilder; +use actix_web::{ + error::ResponseError, + http::{header, StatusCode}, + web, HttpResponse, Responder, +}; +use my_codegen::{get, post}; +use sailfish::TemplateOnce; +use uuid::Uuid; + +use crate::api::v1::auth::runners::{login_runner, Login, Password}; +use crate::api::v1::campaign::runners; +use crate::errors::*; +use crate::pages::auth::sudo::SudoPage; +use crate::AppData; +use crate::PAGES; + +async fn get_title( + username: &str, + uuid: &Uuid, + data: &AppData, +) -> ServiceResult { + struct Name { + name: String, + } + let campaign = sqlx::query_as!( + Name, + "SELECT name + FROM kaizen_campaign + WHERE + uuid = $1 + AND + user_id = (SELECT ID from kaizen_users WHERE name = $2)", + &uuid, + &username + ) + .fetch_one(&data.db) + .await?; + + Ok(format!("Delete camapign \"{}\"?", campaign.name)) +} + +#[get(path = "PAGES.panel.campaigns.delete", wrap = "crate::CheckLogin")] +pub async fn delete_campaign( + id: Identity, + path: web::Path, + data: AppData, +) -> PageResult { + let username = id.identity().unwrap(); + let path = path.into_inner(); + let uuid = Uuid::parse_str(&path).map_err(|_| ServiceError::NotAnId)?; + + let title = get_title(&username, &uuid, &data).await?; + + let page = SudoPage::new( + &crate::PAGES.panel.campaigns.get_delete_route(&path), + &title, + ) + .render_once() + .unwrap(); + Ok(HttpResponse::Ok() + .content_type("text/html; charset=utf-8") + .body(page)) +} + +#[post(path = "PAGES.panel.campaigns.delete", wrap = "crate::CheckLogin")] +pub async fn delete_campaign_submit( + id: Identity, + path: web::Path, + payload: web::Form, + data: AppData, +) -> PageResult { + let username = id.identity().unwrap(); + let path = path.into_inner(); + let uuid = Uuid::parse_str(&path).map_err(|_| ServiceError::NotAnId)?; + let payload = payload.into_inner(); + + async fn render_err( + e: ServiceError, + username: &str, + uuid: &Uuid, + path: &str, + data: &AppData, + ) -> ServiceResult<(String, StatusCode)> { + let status = e.status_code(); + let heading = status.canonical_reason().unwrap_or("Error"); + + let form_route = crate::V1_API_ROUTES.campaign.get_delete_route(&path); + let title = get_title(&username, &uuid, &data).await?; + let mut ctx = SudoPage::new(&form_route, &title); + let err = format!("{}", e); + ctx.set_err(heading, &err); + let page = ctx.render_once().unwrap(); + Ok((page, status)) + } + + let creds = Login { + login: username, + password: payload.password, + }; + + match login_runner(&creds, &data).await { + Err(e) => { + let (page, status) = + render_err(e, &creds.login, &uuid, &path, &data).await?; + + Ok(HttpResponseBuilder::new(status) + .content_type("text/html; charset=utf-8") + .body(page)) + } + Ok(_) => { + match runners::delete(&uuid, &creds.login, &data).await { + Ok(_) => Ok(HttpResponse::Found() + //TODO show stats of new campaign + .insert_header((header::LOCATION, PAGES.panel.campaigns.home)) + .finish()), + + Err(e) => { + let (page, status) = + render_err(e, &creds.login, &uuid, &path, &data).await?; + Ok(HttpResponseBuilder::new(status) + .content_type("text/html; charset=utf-8") + .body(page)) + } + } + } + } +} + +#[cfg(test)] +mod tests { + use actix_web::test; + + use super::*; + + use crate::data::Data; + use crate::tests::*; + use crate::*; + use actix_web::http::StatusCode; + + #[actix_rt::test] + async fn new_campaign_form_works() { + let data = Data::new().await; + const NAME: &str = "delcappageuser"; + const EMAIL: &str = "delcappageuser@aaa.com"; + const PASSWORD: &str = "longpassword"; + const CAMPAIGN_NAME: &str = "delcappageusercamaping"; + + let app = get_app!(data).await; + delete_user(NAME, &data).await; + let (_, _, signin_resp) = register_and_signin(NAME, EMAIL, PASSWORD).await; + let cookies = get_cookie!(signin_resp); + + let uuid = + create_new_campaign(CAMPAIGN_NAME, data.clone(), cookies.clone()).await; + + let creds = Password { + password: PASSWORD.into(), + }; + + let new_resp = test::call_service( + &app, + post_request!( + &creds, + &PAGES.panel.campaigns.get_delete_route(&uuid.uuid), + FORM + ) + .cookie(cookies.clone()) + .to_request(), + ) + .await; + assert_eq!(new_resp.status(), StatusCode::FOUND); + let headers = new_resp.headers(); + assert_eq!( + headers.get(header::LOCATION).unwrap(), + PAGES.panel.campaigns.home, + ); + } +} diff --git a/src/pages/panel/campaigns/get.rs b/src/pages/panel/campaigns/get.rs new file mode 100644 index 0000000..0d8085f --- /dev/null +++ b/src/pages/panel/campaigns/get.rs @@ -0,0 +1,61 @@ +/* + * 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 my_codegen::get; +use sailfish::TemplateOnce; + +use crate::api::v1::campaign::{runners, GetFeedbackResp}; +use crate::AppData; +use crate::PAGES; + +#[derive(TemplateOnce)] +#[template(path = "panel/campaigns/get/index.html")] +struct ViewFeedback<'a> { + campaign: GetFeedbackResp, + uuid: &'a str, +} + +const PAGE: &str = "New Campaign"; + +impl<'a> ViewFeedback<'a> { + pub fn new(campaign: GetFeedbackResp, uuid: &'a str) -> Self { + Self { campaign, uuid } + } +} + +#[get( + path = "PAGES.panel.campaigns.get_feedback", + wrap = "crate::CheckLogin" +)] +pub async fn get_feedback( + id: Identity, + data: AppData, + path: web::Path, +) -> impl Responder { + let username = id.identity().unwrap(); + let path = path.into_inner(); + let feedback_resp = runners::get_feedback(&username, &path, &data) + .await + .unwrap(); + let page = ViewFeedback::new(feedback_resp, &path) + .render_once() + .unwrap(); + HttpResponse::Ok() + .content_type("text/html; charset=utf-8") + .body(page) +} diff --git a/src/pages/panel/campaigns/mod.rs b/src/pages/panel/campaigns/mod.rs new file mode 100644 index 0000000..1fb5541 --- /dev/null +++ b/src/pages/panel/campaigns/mod.rs @@ -0,0 +1,93 @@ +/* + * 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::{HttpResponse, Responder}; +use my_codegen::get; +use sailfish::TemplateOnce; + +use crate::api::v1::campaign::{runners::list_campaign_runner, ListCampaignResp}; +use crate::AppData; +use crate::PAGES; + +pub mod delete; +pub mod get; +pub mod new; + +pub mod routes { + pub struct Campaigns { + pub home: &'static str, + pub new: &'static str, + pub get_feedback: &'static str, + pub delete: &'static str, + } + impl Campaigns { + pub const fn new() -> Campaigns { + Campaigns { + home: "/campaigns", + new: "/campaigns/new", + get_feedback: "/campaigns/{uuid}/feedback", + delete: "/campaigns/{uuid}/delete", + } + } + + pub fn get_delete_route(&self, campaign_id: &str) -> String { + self.delete.replace("{uuid}", campaign_id) + } + + pub fn get_feedback_route(&self, campaign_id: &str) -> String { + self.get_feedback.replace("{uuid}", campaign_id) + } + + pub const fn get_sitemap() -> [&'static str; 2] { + const CAMPAIGNS: Campaigns = Campaigns::new(); + [CAMPAIGNS.home, CAMPAIGNS.new] + } + } +} + +pub fn services(cfg: &mut actix_web::web::ServiceConfig) { + cfg.service(home); + cfg.service(new::new_campaign); + cfg.service(new::new_campaign_submit); + cfg.service(get::get_feedback); + cfg.service(delete::delete_campaign); + cfg.service(delete::delete_campaign_submit); +} + +#[derive(TemplateOnce)] +#[template(path = "panel/campaigns/index.html")] +struct HomePage { + data: Vec, +} + +impl HomePage { + fn new(data: Vec) -> Self { + Self { data } + } +} + +const PAGE: &str = "Campaigns"; + +#[get(path = "PAGES.panel.campaigns.home", wrap = "crate::CheckLogin")] +pub async fn home(data: AppData, id: Identity) -> impl Responder { + let username = id.identity().unwrap(); + let campaigns = list_campaign_runner(&username, &data).await.unwrap(); + let page = HomePage::new(campaigns).render_once().unwrap(); + + HttpResponse::Ok() + .content_type("text/html; charset=utf-8") + .body(&page) +} diff --git a/src/pages/panel/campaigns/new.rs b/src/pages/panel/campaigns/new.rs new file mode 100644 index 0000000..d7ab8ed --- /dev/null +++ b/src/pages/panel/campaigns/new.rs @@ -0,0 +1,134 @@ +/* + * 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::HttpResponseBuilder; +use actix_web::{error::ResponseError, http::header, web, HttpResponse, Responder}; +use lazy_static::lazy_static; +use my_codegen::{get, post}; +use sailfish::TemplateOnce; + +use crate::api::v1::campaign::{runners, CreateReq}; +use crate::errors::*; +use crate::pages::errors::ErrorPage; +use crate::AppData; +use crate::PAGES; + +#[derive(Clone, TemplateOnce)] +#[template(path = "panel/campaigns/new/index.html")] +struct NewCampaign<'a> { + error: Option>, +} + +const PAGE: &str = "New Campaign"; + +impl<'a> Default for NewCampaign<'a> { + fn default() -> Self { + NewCampaign { error: None } + } +} + +impl<'a> NewCampaign<'a> { + pub fn new(title: &'a str, message: &'a str) -> Self { + Self { + error: Some(ErrorPage::new(title, message)), + } + } +} + +lazy_static! { + static ref INDEX: String = NewCampaign::default().render_once().unwrap(); +} + +#[get(path = "PAGES.panel.campaigns.new", wrap = "crate::CheckLogin")] +pub async fn new_campaign() -> impl Responder { + HttpResponse::Ok() + .content_type("text/html; charset=utf-8") + .body(&*INDEX) +} + +#[post(path = "PAGES.panel.campaigns.new", wrap = "crate::CheckLogin")] +pub async fn new_campaign_submit( + id: Identity, + payload: web::Form, + data: AppData, +) -> PageResult { + match runners::new(&payload.into_inner(), &data, &id).await { + Ok(_) => { + Ok(HttpResponse::Found() + //TODO show stats of new campaign + .insert_header((header::LOCATION, PAGES.panel.campaigns.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( + NewCampaign::new(heading, &format!("{}", e)) + .render_once() + .unwrap(), + )) + } + } +} + +#[cfg(test)] +mod tests { + use actix_web::test; + + use super::*; + + use crate::data::Data; + use crate::tests::*; + use crate::*; + use actix_web::http::StatusCode; + + #[actix_rt::test] + async fn new_campaign_form_works() { + let data = Data::new().await; + const NAME: &str = "testusercampaignform"; + const EMAIL: &str = "testcampaignuser@aaa.com"; + const PASSWORD: &str = "longpassword"; + + const CAMPAIGN_NAME: &str = "testcampaignuser"; + + let app = get_app!(data).await; + delete_user(NAME, &data).await; + let (_, _, signin_resp) = register_and_signin(NAME, EMAIL, PASSWORD).await; + let cookies = get_cookie!(signin_resp); + + let new = CreateReq { + name: CAMPAIGN_NAME.into(), + }; + + let new_resp = test::call_service( + &app, + post_request!(&new, PAGES.panel.campaigns.new, FORM) + .cookie(cookies.clone()) + .to_request(), + ) + .await; + assert_eq!(new_resp.status(), StatusCode::FOUND); + let headers = new_resp.headers(); + assert_eq!( + headers.get(header::LOCATION).unwrap(), + PAGES.panel.campaigns.home, + ); + } +} diff --git a/src/pages/panel/mod.rs b/src/pages/panel/mod.rs new file mode 100644 index 0000000..cc7e626 --- /dev/null +++ b/src/pages/panel/mod.rs @@ -0,0 +1,55 @@ +/* + * 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, HttpResponse, Responder}; +use my_codegen::get; + +use crate::PAGES; + +mod campaigns; + +pub mod routes { + use super::campaigns::routes::Campaigns; + + pub struct Panel { + pub home: &'static str, + pub campaigns: Campaigns, + } + impl Panel { + pub const fn new() -> Panel { + Panel { + home: "/", + campaigns: Campaigns::new(), + } + } + + pub const fn get_sitemap() -> [&'static str; 1] { + const PANEL: Panel = Panel::new(); + [PANEL.home] + } + } +} + +pub fn services(cfg: &mut actix_web::web::ServiceConfig) { + cfg.service(home); + campaigns::services(cfg); +} + +#[get(path = "PAGES.panel.home", wrap = "crate::CheckLogin")] +pub async fn home() -> impl Responder { + HttpResponse::Found() + .insert_header((http::header::LOCATION, PAGES.panel.campaigns.home)) + .finish() +} diff --git a/src/pages/routes.rs b/src/pages/routes.rs new file mode 100644 index 0000000..7547947 --- /dev/null +++ b/src/pages/routes.rs @@ -0,0 +1,68 @@ +/* + * 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 super::auth::routes::Auth; +use super::errors::routes::Errors; +use super::panel::routes::Panel; +pub const ROUTES: Routes = Routes::new(); + +pub struct Routes { + pub home: &'static str, + pub auth: Auth, + pub panel: Panel, + pub errors: Errors, + pub about: &'static str, + pub sitemap: &'static str, + pub thanks: &'static str, + pub donate: &'static str, + pub security: &'static str, + pub privacy: &'static str, +} + +impl Routes { + const fn new() -> Routes { + let panel = Panel::new(); + let home = panel.home; + Routes { + auth: Auth::new(), + panel, + home, + errors: Errors::new(), + about: "/about", + sitemap: "/sitemap.xml", + thanks: "/thanks", + donate: "/donate", + security: "/security", + privacy: "/privacy-policy", + } + } + + pub const fn get_sitemap() -> [&'static str; 3] { + let a = Auth::get_sitemap(); + let p = Panel::get_sitemap(); + [a[0], a[1], p[0]] //, p[1], p[2], p[3], p[4]] + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn sitemap_works() { + Routes::get_sitemap(); + } +} diff --git a/src/settings.rs b/src/settings.rs new file mode 100644 index 0000000..20116d5 --- /dev/null +++ b/src/settings.rs @@ -0,0 +1,180 @@ +/* + * 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::env; +use std::path::Path; + +use config::{Config, ConfigError, Environment, File}; +use log::{debug, warn}; +use serde::Deserialize; +use url::Url; + +#[derive(Debug, Clone, Deserialize)] +pub struct Server { + pub port: u32, + pub domain: String, + pub cookie_secret: String, + pub ip: String, + pub proxy_has_tls: bool, +} + +impl Server { + #[cfg(not(tarpaulin_include))] + pub fn get_ip(&self) -> String { + format!("{}:{}", self.ip, self.port) + } +} + +#[derive(Debug, Clone, Deserialize)] +struct DatabaseBuilder { + pub port: u32, + pub hostname: String, + pub username: String, + pub password: String, + pub name: String, + pub url: String, +} + +impl DatabaseBuilder { + #[cfg(not(tarpaulin_include))] + fn extract_database_url(url: &Url) -> Self { + debug!("Databse name: {}", url.path()); + let mut path = url.path().split('/'); + path.next(); + let name = path.next().expect("no database name").to_string(); + DatabaseBuilder { + port: url.port().expect("Enter database port").into(), + hostname: url.host().expect("Enter database host").to_string(), + username: url.username().into(), + url: url.to_string(), + password: url.password().expect("Enter database password").into(), + name, + } + } +} + +#[derive(Debug, Clone, Deserialize)] +pub struct Database { + pub url: String, + pub pool: u32, +} + +#[derive(Debug, Clone, Deserialize)] +pub struct Settings { + pub debug: bool, + pub allow_registration: bool, + pub database: Database, + pub server: Server, + pub source_code: String, + pub password: String, +} + +#[cfg(not(tarpaulin_include))] +impl Settings { + pub fn new() -> Result { + let mut s = Config::new(); + + // setting default values + #[cfg(test)] + s.set_default("database.pool", 2.to_string()) + .expect("Couldn't get the number of CPUs"); + + const CURRENT_DIR: &str = "./config/default.toml"; + const ETC: &str = "/etc/athena/config.toml"; + + if let Ok(path) = env::var("ATHENA_CONFIG") { + s.merge(File::with_name(&path))?; + } else if Path::new(CURRENT_DIR).exists() { + // merging default config from file + s.merge(File::with_name(CURRENT_DIR))?; + } else if Path::new(ETC).exists() { + s.merge(File::with_name(ETC))?; + } else { + log::warn!("configuration file not found"); + } + + s.merge(Environment::with_prefix("MCAPTCHA").separator("_"))?; + + check_url(&s); + + match env::var("PORT") { + Ok(val) => { + s.set("server.port", val).unwrap(); + } + Err(e) => warn!("couldn't interpret PORT: {}", e), + } + + match env::var("DATABASE_URL") { + Ok(val) => { + let url = Url::parse(&val).expect("couldn't parse Database URL"); + let database_conf = DatabaseBuilder::extract_database_url(&url); + set_from_database_url(&mut s, &database_conf); + } + Err(e) => warn!("couldn't interpret DATABASE_URL: {}", e), + } + + set_database_url(&mut s); + + match s.try_into() { + Ok(val) => Ok(val), + Err(e) => Err(ConfigError::Message(format!("\n\nError: {}. If it says missing fields, then please refer to https://github.com/mCaptcha/mcaptcha#configuration to learn more about how mcaptcha reads configuration\n\n", e))), + } + } +} + +#[cfg(not(tarpaulin_include))] +fn check_url(s: &Config) { + let url = s + .get::("source_code") + .expect("Couldn't access source_code"); + + Url::parse(&url).expect("Please enter a URL for source_code in settings"); +} + +#[cfg(not(tarpaulin_include))] +fn set_from_database_url(s: &mut Config, database_conf: &DatabaseBuilder) { + s.set("database.username", database_conf.username.clone()) + .expect("Couldn't set database username"); + s.set("database.password", database_conf.password.clone()) + .expect("Couldn't access database password"); + s.set("database.hostname", database_conf.hostname.clone()) + .expect("Couldn't access database hostname"); + s.set("database.port", database_conf.port as i64) + .expect("Couldn't access database port"); + s.set("database.name", database_conf.name.clone()) + .expect("Couldn't access database name"); +} + +#[cfg(not(tarpaulin_include))] +fn set_database_url(s: &mut Config) { + s.set( + "database.url", + format!( + r"postgres://{}:{}@{}:{}/{}", + s.get::("database.username") + .expect("Couldn't access database username"), + s.get::("database.password") + .expect("Couldn't access database password"), + s.get::("database.hostname") + .expect("Couldn't access database hostname"), + s.get::("database.port") + .expect("Couldn't access database port"), + s.get::("database.name") + .expect("Couldn't access database name") + ), + ) + .expect("Couldn't set databse url"); +} diff --git a/src/static_assets/filemap.rs b/src/static_assets/filemap.rs new file mode 100644 index 0000000..4f7a364 --- /dev/null +++ b/src/static_assets/filemap.rs @@ -0,0 +1,46 @@ +/* + * 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 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/img/logo.svg").unwrap(); + println!("{}", css); + assert!(css.contains("/assets/img/logo")); + } +} diff --git a/src/static_assets/mod.rs b/src/static_assets/mod.rs new file mode 100644 index 0000000..a080139 --- /dev/null +++ b/src/static_assets/mod.rs @@ -0,0 +1,25 @@ +/* + * 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 . + */ +pub mod filemap; +pub mod static_files; + +pub use filemap::FileMap; + +pub fn services(cfg: &mut actix_web::web::ServiceConfig) { + cfg.service(static_files::static_files); + cfg.service(static_files::favicons); +} diff --git a/src/static_assets/static_files.rs b/src/static_assets/static_files.rs new file mode 100644 index 0000000..38dcdf4 --- /dev/null +++ b/src/static_assets/static_files.rs @@ -0,0 +1,145 @@ +/* + * 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::borrow::Cow; + +use actix_web::body::Body; +use actix_web::{get, http::header, web, HttpResponse, Responder}; +use log::debug; +use mime_guess::from_path; +use rust_embed::RustEmbed; + +use crate::CACHE_AGE; + +pub mod assets { + use lazy_static::lazy_static; + + use crate::FILES; + + pub struct Img { + pub path: &'static str, + pub name: &'static str, + } + + lazy_static! { + pub static ref LOGO: Img = Img { + path: FILES.get("./static/cache/img/logo.svg").unwrap(), + name: "Kaizen logo" + }; + pub static ref TRASH: Img = Img { + path: FILES.get("./static/cache/img/trash.svg").unwrap(), + name: "Trash logo" + }; + } +} + +#[derive(RustEmbed)] +#[folder = "assets/"] +struct Asset; + +fn handle_assets(path: &str) -> HttpResponse { + match Asset::get(path) { + Some(content) => { + let body: Body = match content.data { + Cow::Borrowed(bytes) => bytes.into(), + Cow::Owned(bytes) => bytes.into(), + }; + + 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) +} + +#[derive(RustEmbed)] +#[folder = "static/favicons/"] +struct Favicons; + +fn handle_favicons(path: &str) -> HttpResponse { + match Favicons::get(path) { + Some(content) => { + let body: Body = match content.data { + Cow::Borrowed(bytes) => bytes.into(), + Cow::Owned(bytes) => bytes.into(), + }; + + 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("/{file}")] +pub async fn favicons(path: web::Path) -> impl Responder { + debug!("searching favicons"); + handle_favicons(&path) +} + +#[cfg(test)] +mod tests { + use actix_web::http::StatusCode; + use actix_web::test; + + use super::*; + use crate::*; + + #[actix_rt::test] + async fn static_assets_work() { + let app = get_app!().await; + + for file in [assets::LOGO.path, *crate::CSS].iter() { + let resp = test::call_service( + &app, + test::TestRequest::get().uri(file).to_request(), + ) + .await; + assert_eq!(resp.status(), StatusCode::OK); + } + } + + #[actix_rt::test] + async fn favicons_work() { + assert!(Favicons::get("favicon.ico").is_some()); + + let app = get_app!().await; + + let resp = test::call_service( + &app, + test::TestRequest::get().uri("/favicon.ico").to_request(), + ) + .await; + assert_eq!(resp.status(), StatusCode::OK); + } +} diff --git a/src/tests-migrate.rs b/src/tests-migrate.rs new file mode 100644 index 0000000..569dd50 --- /dev/null +++ b/src/tests-migrate.rs @@ -0,0 +1,92 @@ +/* + * 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::env; + +use lazy_static::lazy_static; +use sqlx::postgres::PgPoolOptions; + +mod settings; + +pub use settings::Settings; + +#[cfg(not(tarpaulin_include))] +lazy_static! { + #[cfg(not(tarpaulin_include))] + pub static ref SETTINGS: Settings = Settings::new().unwrap(); +} + +#[cfg(not(tarpaulin_include))] +#[actix_rt::main] +async fn main() { + let db = PgPoolOptions::new() + .max_connections(SETTINGS.database.pool) + .connect(&SETTINGS.database.url) + .await + .expect("Unable to form database pool"); + + for arg in env::args() { + if arg == "--build" { + println!("Building cache buster config"); + build(); + } + } + + sqlx::migrate!("./migrations/").run(&db).await.unwrap(); +} + +fn build() { + use std::process::Command; + + // note: add error checking yourself. + let output = Command::new("git") + .args(&["rev-parse", "HEAD"]) + .output() + .unwrap(); + let git_hash = String::from_utf8(output.stdout).unwrap(); + println!("cargo:rustc-env=GIT_HASH={}", git_hash); + + // let yml = include_str!("../openapi.yaml"); + // let api_json: serde_json::Value = serde_yaml::from_str(yml).unwrap(); + // println!( + // "cargo:rustc-env=OPEN_API_DOCS={}", + // serde_json::to_string(&api_json).unwrap() + // ); + cache_bust(); +} + +fn cache_bust() { + use cache_buster::BusterBuilder; + let types = vec![ + mime::IMAGE_PNG, + mime::IMAGE_SVG, + mime::IMAGE_JPEG, + mime::IMAGE_GIF, + mime::APPLICATION_JAVASCRIPT, + mime::TEXT_CSS, + ]; + + let config = BusterBuilder::default() + .source("./static/cache") + .result("./assets") + .mime_types(types) + .copy(true) + .follow_links(true) + .build() + .unwrap(); + + config.process().unwrap(); +} diff --git a/src/tests.rs b/src/tests.rs new file mode 100644 index 0000000..098af8a --- /dev/null +++ b/src/tests.rs @@ -0,0 +1,261 @@ +use std::sync::Arc; + +use actix_web::cookie::Cookie; +use actix_web::test; +use actix_web::{dev::ServiceResponse, error::ResponseError, http::StatusCode}; +use serde::Serialize; + +use super::*; +use crate::api::v1::feedback::{RatingReq, RatingResp}; +use crate::api::v1::ROUTES; +use crate::data::Data; +use crate::errors::*; + +#[macro_export] +macro_rules! get_cookie { + ($resp:expr) => { + $resp.response().cookies().next().unwrap().to_owned() + }; +} + +pub async fn delete_user(name: &str, data: &Data) { + let r = sqlx::query!("DELETE FROM kaizen_users WHERE name = ($1)", name,) + .execute(&data.db) + .await; + println!(); + println!(); + println!(); + println!("Deleting user: {:?}", &r); +} + +#[allow(dead_code, clippy::upper_case_acronyms)] +pub struct FORM; + +#[macro_export] +macro_rules! post_request { + ($uri:expr) => { + test::TestRequest::post().uri($uri) + }; + + ($serializable:expr, $uri:expr) => { + 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) => { + test::TestRequest::post().uri($uri).set_form($serializable) + }; +} + +#[macro_export] +macro_rules! get_works { + ($app:expr,$route:expr ) => { + let list_sitekey_resp = + test::call_service(&$app, test::TestRequest::get().uri($route).to_request()) + .await; + assert_eq!(list_sitekey_resp.status(), StatusCode::OK); + }; +} + +#[macro_export] +macro_rules! get_app { + ("APP") => { + actix_web::App::new() + .app_data(crate::get_json_err()) + .wrap(crate::get_identity_service()) + .wrap(actix_web::middleware::NormalizePath::new( + actix_web::middleware::TrailingSlash::Trim, + )) + .configure(crate::services) + }; + + () => { + test::init_service(get_app!("APP")) + }; + ($data:expr) => { + test::init_service( + get_app!("APP").app_data(actix_web::web::Data::new($data.clone())), + ) + }; +} + +/// register and signin utility +pub async fn register_and_signin( + name: &str, + email: &str, + password: &str, +) -> (Arc, Login, ServiceResponse) { + register(name, email, password).await; + signin(name, password).await +} + +/// register utility +pub async fn register(name: &str, email: &str, password: &str) { + let data = Data::new().await; + let app = get_app!(data).await; + + // 1. Register + let msg = Register { + username: name.into(), + password: password.into(), + confirm_password: password.into(), + email: Some(email.into()), + }; + let resp = + test::call_service(&app, post_request!(&msg, ROUTES.auth.register).to_request()) + .await; + assert_eq!(resp.status(), StatusCode::OK); +} + +/// signin util +pub async fn signin(name: &str, password: &str) -> (Arc, Login, ServiceResponse) { + let data = Data::new().await; + let app = get_app!(data.clone()).await; + + // 2. signin + let creds = Login { + login: name.into(), + password: password.into(), + }; + let signin_resp = + test::call_service(&app, post_request!(&creds, ROUTES.auth.login).to_request()) + .await; + assert_eq!(signin_resp.status(), StatusCode::OK); + (data, creds, signin_resp) +} + +/// pub duplicate test +pub async fn bad_post_req_test( + name: &str, + password: &str, + url: &str, + payload: &T, + err: ServiceError, +) { + let (data, _, signin_resp) = signin(name, password).await; + let cookies = get_cookie!(signin_resp); + let app = get_app!(data).await; + + let resp = 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 = 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( + name: &str, + password: &str, + url: &str, + err: ServiceError, +) { + let (data, _, signin_resp) = signin(name, password).await; + let cookies = get_cookie!(signin_resp); + let app = get_app!(data).await; + + let resp = test::call_service( + &app, + post_request!(url).cookie(cookies.clone()).to_request(), + ) + .await; + assert_eq!(resp.status(), err.status_code()); + let resp_err: ErrorToResponse = test::read_body_json(resp).await; + //println!("{}", txt.error); + assert_eq!(resp_err.error, format!("{}", err)); +} + +pub async fn create_new_campaign( + campaign_name: &str, + data: Arc, + cookies: Cookie<'_>, +) -> CreateResp { + let new = CreateReq { + name: campaign_name.into(), + }; + + let app = get_app!(data).await; + let new_resp = test::call_service( + &app, + post_request!(&new, crate::V1_API_ROUTES.campaign.new) + .cookie(cookies) + .to_request(), + ) + .await; + assert_eq!(new_resp.status(), StatusCode::OK); + let uuid: CreateResp = test::read_body_json(new_resp).await; + uuid +} + +pub async fn delete_campaign( + camapign: &CreateResp, + data: Arc, + cookies: Cookie<'_>, +) { + let del_route = V1_API_ROUTES.campaign.get_delete_route(&camapign.uuid); + let app = get_app!(data).await; + let del_resp = + test::call_service(&app, post_request!(&del_route).cookie(cookies).to_request()) + .await; + assert_eq!(del_resp.status(), StatusCode::OK); +} + +pub async fn list_campaings( + data: Arc, + cookies: Cookie<'_>, +) -> Vec { + let app = get_app!(data).await; + let list_resp = test::call_service( + &app, + post_request!(crate::V1_API_ROUTES.campaign.list) + .cookie(cookies) + .to_request(), + ) + .await; + assert_eq!(list_resp.status(), StatusCode::OK); + test::read_body_json(list_resp).await +} + +pub async fn add_feedback( + rating: &RatingReq, + campaign: &CreateResp, + data: Arc, +) -> RatingResp { + let add_feedback_route = V1_API_ROUTES.feedback.add_feedback_route(&campaign.uuid); + let app = get_app!(data).await; + let add_feedback_resp = test::call_service( + &app, + post_request!(&rating, &add_feedback_route).to_request(), + ) + .await; + assert_eq!(add_feedback_resp.status(), StatusCode::OK); + + test::read_body_json(add_feedback_resp).await +} + +pub async fn get_feedback( + campaign: &CreateResp, + data: Arc, + cookies: Cookie<'_>, +) -> GetFeedbackResp { + let get_feedback_route = V1_API_ROUTES.campaign.get_feedback_route(&campaign.uuid); + let app = get_app!(data).await; + + let get_feedback_resp = test::call_service( + &app, + post_request!(&get_feedback_route) + .cookie(cookies) + .to_request(), + ) + .await; + assert_eq!(get_feedback_resp.status(), StatusCode::OK); + test::read_body_json(get_feedback_resp).await +} diff --git a/static/cache/img/logo.png b/static/cache/img/logo.png new file mode 100644 index 0000000..c92af77 Binary files /dev/null and b/static/cache/img/logo.png differ diff --git a/static/cache/img/logo.svg b/static/cache/img/logo.svg new file mode 100644 index 0000000..eadecdf --- /dev/null +++ b/static/cache/img/logo.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/static/cache/img/trash.svg b/static/cache/img/trash.svg new file mode 100644 index 0000000..f24d55b --- /dev/null +++ b/static/cache/img/trash.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/static/favicons/android-icon-144x144.png b/static/favicons/android-icon-144x144.png new file mode 100644 index 0000000..9a1cd14 Binary files /dev/null and b/static/favicons/android-icon-144x144.png differ diff --git a/static/favicons/android-icon-192x192.png b/static/favicons/android-icon-192x192.png new file mode 100644 index 0000000..18aa76b Binary files /dev/null and b/static/favicons/android-icon-192x192.png differ diff --git a/static/favicons/android-icon-36x36.png b/static/favicons/android-icon-36x36.png new file mode 100644 index 0000000..aa752c4 Binary files /dev/null and b/static/favicons/android-icon-36x36.png differ diff --git a/static/favicons/android-icon-48x48.png b/static/favicons/android-icon-48x48.png new file mode 100644 index 0000000..297b33c Binary files /dev/null and b/static/favicons/android-icon-48x48.png differ diff --git a/static/favicons/android-icon-72x72.png b/static/favicons/android-icon-72x72.png new file mode 100644 index 0000000..41d610d Binary files /dev/null and b/static/favicons/android-icon-72x72.png differ diff --git a/static/favicons/android-icon-96x96.png b/static/favicons/android-icon-96x96.png new file mode 100644 index 0000000..dd034f1 Binary files /dev/null and b/static/favicons/android-icon-96x96.png differ diff --git a/static/favicons/apple-icon-114x114.png b/static/favicons/apple-icon-114x114.png new file mode 100644 index 0000000..d1f6536 Binary files /dev/null and b/static/favicons/apple-icon-114x114.png differ diff --git a/static/favicons/apple-icon-120x120.png b/static/favicons/apple-icon-120x120.png new file mode 100644 index 0000000..bba9790 Binary files /dev/null and b/static/favicons/apple-icon-120x120.png differ diff --git a/static/favicons/apple-icon-144x144.png b/static/favicons/apple-icon-144x144.png new file mode 100644 index 0000000..9a1cd14 Binary files /dev/null and b/static/favicons/apple-icon-144x144.png differ diff --git a/static/favicons/apple-icon-152x152.png b/static/favicons/apple-icon-152x152.png new file mode 100644 index 0000000..7461929 Binary files /dev/null and b/static/favicons/apple-icon-152x152.png differ diff --git a/static/favicons/apple-icon-180x180.png b/static/favicons/apple-icon-180x180.png new file mode 100644 index 0000000..215398c Binary files /dev/null and b/static/favicons/apple-icon-180x180.png differ diff --git a/static/favicons/apple-icon-57x57.png b/static/favicons/apple-icon-57x57.png new file mode 100644 index 0000000..37e1141 Binary files /dev/null and b/static/favicons/apple-icon-57x57.png differ diff --git a/static/favicons/apple-icon-60x60.png b/static/favicons/apple-icon-60x60.png new file mode 100644 index 0000000..82be2ef Binary files /dev/null and b/static/favicons/apple-icon-60x60.png differ diff --git a/static/favicons/apple-icon-72x72.png b/static/favicons/apple-icon-72x72.png new file mode 100644 index 0000000..41d610d Binary files /dev/null and b/static/favicons/apple-icon-72x72.png differ diff --git a/static/favicons/apple-icon-76x76.png b/static/favicons/apple-icon-76x76.png new file mode 100644 index 0000000..b0b6f99 Binary files /dev/null and b/static/favicons/apple-icon-76x76.png differ diff --git a/static/favicons/apple-icon-precomposed.png b/static/favicons/apple-icon-precomposed.png new file mode 100644 index 0000000..7b5e161 Binary files /dev/null and b/static/favicons/apple-icon-precomposed.png differ diff --git a/static/favicons/apple-icon.png b/static/favicons/apple-icon.png new file mode 100644 index 0000000..7b5e161 Binary files /dev/null and b/static/favicons/apple-icon.png differ diff --git a/static/favicons/browserconfig.xml b/static/favicons/browserconfig.xml new file mode 100644 index 0000000..c554148 --- /dev/null +++ b/static/favicons/browserconfig.xml @@ -0,0 +1,2 @@ + +#ffffff \ No newline at end of file diff --git a/static/favicons/favicon-16x16.png b/static/favicons/favicon-16x16.png new file mode 100644 index 0000000..bbeb5ae Binary files /dev/null and b/static/favicons/favicon-16x16.png differ diff --git a/static/favicons/favicon-32x32.png b/static/favicons/favicon-32x32.png new file mode 100644 index 0000000..0321c3f Binary files /dev/null and b/static/favicons/favicon-32x32.png differ diff --git a/static/favicons/favicon-96x96.png b/static/favicons/favicon-96x96.png new file mode 100644 index 0000000..dd034f1 Binary files /dev/null and b/static/favicons/favicon-96x96.png differ diff --git a/static/favicons/favicon.ico b/static/favicons/favicon.ico new file mode 100644 index 0000000..0f6b226 Binary files /dev/null and b/static/favicons/favicon.ico differ diff --git a/static/favicons/manifest.json b/static/favicons/manifest.json new file mode 100644 index 0000000..013d4a6 --- /dev/null +++ b/static/favicons/manifest.json @@ -0,0 +1,41 @@ +{ + "name": "App", + "icons": [ + { + "src": "\/android-icon-36x36.png", + "sizes": "36x36", + "type": "image\/png", + "density": "0.75" + }, + { + "src": "\/android-icon-48x48.png", + "sizes": "48x48", + "type": "image\/png", + "density": "1.0" + }, + { + "src": "\/android-icon-72x72.png", + "sizes": "72x72", + "type": "image\/png", + "density": "1.5" + }, + { + "src": "\/android-icon-96x96.png", + "sizes": "96x96", + "type": "image\/png", + "density": "2.0" + }, + { + "src": "\/android-icon-144x144.png", + "sizes": "144x144", + "type": "image\/png", + "density": "3.0" + }, + { + "src": "\/android-icon-192x192.png", + "sizes": "192x192", + "type": "image\/png", + "density": "4.0" + } + ] +} \ No newline at end of file diff --git a/static/favicons/ms-icon-144x144.png b/static/favicons/ms-icon-144x144.png new file mode 100644 index 0000000..9a1cd14 Binary files /dev/null and b/static/favicons/ms-icon-144x144.png differ diff --git a/static/favicons/ms-icon-150x150.png b/static/favicons/ms-icon-150x150.png new file mode 100644 index 0000000..6b8d2a9 Binary files /dev/null and b/static/favicons/ms-icon-150x150.png differ diff --git a/static/favicons/ms-icon-310x310.png b/static/favicons/ms-icon-310x310.png new file mode 100644 index 0000000..b75c388 Binary files /dev/null and b/static/favicons/ms-icon-310x310.png differ diff --git a/static/favicons/ms-icon-70x70.png b/static/favicons/ms-icon-70x70.png new file mode 100644 index 0000000..51b1dc1 Binary files /dev/null and b/static/favicons/ms-icon-70x70.png differ