From a297a965aa2f270d5b0dea62f6d20e9097bd3f53 Mon Sep 17 00:00:00 2001 From: Jane Petrovna Date: Thu, 12 Aug 2021 21:23:03 -0400 Subject: [PATCH] switch to rust, implement discord login and switch to svelte --- .gitignore | 9 + backend/.env.example | 3 + backend/.prettierrc.yaml | 16 - backend/Cargo.lock | 2418 +++++++++++++++++ backend/Cargo.toml | 33 + backend/README.md | 2 - backend/config.json.example | 16 - backend/default.profraw | 0 backend/diesel.toml | 5 + backend/env.sh | 26 + backend/migrations/.gitkeep | 0 .../down.sql | 6 + .../up.sql | 36 + .../2021-08-05-011028_create_user/down.sql | 2 + .../2021-08-05-011028_create_user/up.sql | 9 + .../2021-08-06-173217_create_block/down.sql | 2 + .../2021-08-06-173217_create_block/up.sql | 12 + backend/package.json | 36 - backend/src/config.js | 21 - backend/src/db_interface.js | 20 - backend/src/endpoints.rs | 30 + backend/src/endpoints/block.rs | 0 backend/src/endpoints/discord.rs | 214 ++ backend/src/endpoints/user.rs | 1 + backend/src/helpers.rs | 2 + backend/src/helpers/block.rs | 56 + backend/src/helpers/user.rs | 51 + backend/src/index.js | 70 - backend/src/logging.rs | 26 + backend/src/mail.js | 52 - backend/src/main.rs | 66 + backend/src/migration.rs | 22 + backend/src/models.js | 141 - backend/src/models.rs | 38 + backend/src/schema.rs | 47 + backend/src/tests.rs | 18 + backend/src/tests/db.rs | 132 + backend/src/todo.js | 146 - backend/src/user.js | 424 --- backend/test/db_interface.spec.js | 17 - backend/test/user.spec.js | 10 - frontend/.gitignore | 23 - frontend/README.md | 111 +- frontend/package.json | 59 +- frontend/public/build/bundle.css | 1 + frontend/public/build/bundle.js | 801 ++++++ frontend/public/build/bundle.js.map | 1 + frontend/public/favicon.png | Bin 0 -> 3127 bytes frontend/public/global.css | 63 + frontend/public/index.html | 49 +- frontend/rollup.config.js | 76 + frontend/scripts/setupTypeScript.js | 121 + frontend/src/App.js | 82 - frontend/src/App.svelte | 90 + frontend/src/Navbar.js | 122 - frontend/src/ThemeProvider.js | 37 - frontend/src/index.js | 79 - frontend/src/main.js | 7 + frontend/src/modules/About/index.js | 5 - frontend/src/modules/Account/index.js | 5 - frontend/src/modules/Login/index.js | 174 -- frontend/src/modules/Oauth/index.js | 17 - frontend/src/modules/Root/index.js | 5 - frontend/src/modules/Signup/index.js | 162 -- frontend/src/modules/TodoList/index.js | 5 - frontend/src/reducers/config.js | 28 - frontend/src/reducers/index.js | 10 - frontend/src/reducers/localStorage.js | 28 - frontend/src/reducers/login.js | 120 - frontend/src/reducers/utils.js | 16 - frontend/src/reportWebVitals.js | 13 - frontend/src/setupTests.js | 5 - frontend/src/theme.js | 13 - 73 files changed, 4525 insertions(+), 2038 deletions(-) create mode 100644 backend/.env.example delete mode 100644 backend/.prettierrc.yaml create mode 100644 backend/Cargo.lock create mode 100644 backend/Cargo.toml delete mode 100644 backend/README.md delete mode 100644 backend/config.json.example create mode 100644 backend/default.profraw create mode 100644 backend/diesel.toml create mode 100755 backend/env.sh create mode 100644 backend/migrations/.gitkeep create mode 100644 backend/migrations/00000000000000_diesel_initial_setup/down.sql create mode 100644 backend/migrations/00000000000000_diesel_initial_setup/up.sql create mode 100644 backend/migrations/2021-08-05-011028_create_user/down.sql create mode 100644 backend/migrations/2021-08-05-011028_create_user/up.sql create mode 100644 backend/migrations/2021-08-06-173217_create_block/down.sql create mode 100644 backend/migrations/2021-08-06-173217_create_block/up.sql delete mode 100644 backend/package.json delete mode 100644 backend/src/config.js delete mode 100644 backend/src/db_interface.js create mode 100644 backend/src/endpoints.rs create mode 100644 backend/src/endpoints/block.rs create mode 100644 backend/src/endpoints/discord.rs create mode 100644 backend/src/endpoints/user.rs create mode 100644 backend/src/helpers.rs create mode 100644 backend/src/helpers/block.rs create mode 100644 backend/src/helpers/user.rs delete mode 100644 backend/src/index.js create mode 100644 backend/src/logging.rs delete mode 100644 backend/src/mail.js create mode 100644 backend/src/main.rs create mode 100644 backend/src/migration.rs delete mode 100644 backend/src/models.js create mode 100644 backend/src/models.rs create mode 100644 backend/src/schema.rs create mode 100644 backend/src/tests.rs create mode 100644 backend/src/tests/db.rs delete mode 100644 backend/src/todo.js delete mode 100644 backend/src/user.js delete mode 100644 backend/test/db_interface.spec.js delete mode 100644 backend/test/user.spec.js delete mode 100644 frontend/.gitignore create mode 100644 frontend/public/build/bundle.css create mode 100644 frontend/public/build/bundle.js create mode 100644 frontend/public/build/bundle.js.map create mode 100644 frontend/public/favicon.png create mode 100644 frontend/public/global.css create mode 100644 frontend/rollup.config.js create mode 100644 frontend/scripts/setupTypeScript.js delete mode 100644 frontend/src/App.js create mode 100644 frontend/src/App.svelte delete mode 100644 frontend/src/Navbar.js delete mode 100644 frontend/src/ThemeProvider.js delete mode 100644 frontend/src/index.js create mode 100644 frontend/src/main.js delete mode 100644 frontend/src/modules/About/index.js delete mode 100644 frontend/src/modules/Account/index.js delete mode 100644 frontend/src/modules/Login/index.js delete mode 100644 frontend/src/modules/Oauth/index.js delete mode 100644 frontend/src/modules/Root/index.js delete mode 100644 frontend/src/modules/Signup/index.js delete mode 100644 frontend/src/modules/TodoList/index.js delete mode 100644 frontend/src/reducers/config.js delete mode 100644 frontend/src/reducers/index.js delete mode 100644 frontend/src/reducers/localStorage.js delete mode 100644 frontend/src/reducers/login.js delete mode 100644 frontend/src/reducers/utils.js delete mode 100644 frontend/src/reportWebVitals.js delete mode 100644 frontend/src/setupTests.js delete mode 100644 frontend/src/theme.js diff --git a/.gitignore b/.gitignore index 50a664d..d6e19f5 100644 --- a/.gitignore +++ b/.gitignore @@ -42,6 +42,11 @@ build/Release node_modules/ jspm_packages/ +/public/build/ + +.DS_Store + + # Snowpack dependency directory (https://snowpack.dev/) web_modules/ @@ -108,6 +113,7 @@ dist # Stores VSCode versions used for testing VSCode extensions .vscode-test +.vscode/ # yarn v2 .yarn/cache @@ -127,3 +133,6 @@ pnpm-lock.yaml backend/config.json *.pem +.env +test_coverage/ +target/ diff --git a/backend/.env.example b/backend/.env.example new file mode 100644 index 0000000..8c48d49 --- /dev/null +++ b/backend/.env.example @@ -0,0 +1,3 @@ +DATABASE_URL=postgres://postgres@localhost/todo_dev +LOG_LEVEL=DEBUG +PORT=8080 \ No newline at end of file diff --git a/backend/.prettierrc.yaml b/backend/.prettierrc.yaml deleted file mode 100644 index ef5eace..0000000 --- a/backend/.prettierrc.yaml +++ /dev/null @@ -1,16 +0,0 @@ -arrowParens: 'always' -bracketSpacing: true -endOfLine: 'lf' -htmlWhitespaceSensitivity: 'css' -insertPragma: false -jsxBracketSameLine: true -jsxSingleQuote: true -printWidth: 120 -proseWrap: 'preserve' -quoteProps: 'consistent' -requirePragma: false -semi: true -singleQuote: true -tabWidth: 2 -trailingComma: 'none' -useTabs: false diff --git a/backend/Cargo.lock b/backend/Cargo.lock new file mode 100644 index 0000000..5a8455a --- /dev/null +++ b/backend/Cargo.lock @@ -0,0 +1,2418 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 3 + +[[package]] +name = "adler" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe" + +[[package]] +name = "aho-corasick" +version = "0.7.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e37cfd5e7657ada45f742d6e99ca5788580b5c529dc78faf11ece6dc702656f" +dependencies = [ + "memchr", +] + +[[package]] +name = "anyhow" +version = "1.0.42" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "595d3cfa7a60d4555cb5067b99f07142a08ea778de5cf993f7b75c7d8fabc486" + +[[package]] +name = "arrayref" +version = "0.3.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4c527152e37cf757a3f78aae5a06fbeefdb07ccc535c980a3208ee3060dd544" + +[[package]] +name = "arrayvec" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "23b62fc65de8e4e7f52534fb52b0f3ed04746ae267519eef2a83941e8085068b" + +[[package]] +name = "async-attributes" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a3203e79f4dd9bdda415ed03cf14dae5a2bf775c683a00f94e9cd1faf0f596e5" +dependencies = [ + "quote", + "syn", +] + +[[package]] +name = "async-channel" +version = "1.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2114d64672151c0c5eaa5e131ec84a74f06e1e559830dabba01ca30605d66319" +dependencies = [ + "concurrent-queue", + "event-listener", + "futures-core", +] + +[[package]] +name = "async-executor" +version = "1.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "871f9bb5e0a22eeb7e8cf16641feb87c9dc67032ccf8ff49e772eb9941d3a965" +dependencies = [ + "async-task", + "concurrent-queue", + "fastrand", + "futures-lite", + "once_cell", + "slab", +] + +[[package]] +name = "async-global-executor" +version = "2.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9586ec52317f36de58453159d48351bc244bc24ced3effc1fce22f3d48664af6" +dependencies = [ + "async-channel", + "async-executor", + "async-io", + "async-mutex", + "blocking", + "futures-lite", + "num_cpus", + "once_cell", +] + +[[package]] +name = "async-io" +version = "1.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a811e6a479f2439f0c04038796b5cfb3d2ad56c230e0f2d3f7b04d68cfee607b" +dependencies = [ + "concurrent-queue", + "futures-lite", + "libc", + "log", + "once_cell", + "parking", + "polling", + "slab", + "socket2", + "waker-fn", + "winapi 0.3.9", +] + +[[package]] +name = "async-lock" +version = "2.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6a8ea61bf9947a1007c5cada31e647dbc77b103c679858150003ba697ea798b" +dependencies = [ + "event-listener", +] + +[[package]] +name = "async-mutex" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "479db852db25d9dbf6204e6cb6253698f175c15726470f78af0d918e99d6156e" +dependencies = [ + "event-listener", +] + +[[package]] +name = "async-redis-session" +version = "0.2.2" +source = "git+https://github.com/jbr/async-redis-session#1bf510696b646dd207a6b13bf1939587c88db997" +dependencies = [ + "async-session", + "redis 0.21.0", +] + +[[package]] +name = "async-session" +version = "3.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "07da4ce523b4e2ebaaf330746761df23a465b951a83d84bbce4233dabedae630" +dependencies = [ + "anyhow", + "async-lock", + "async-trait", + "base64 0.13.0", + "bincode", + "blake3", + "chrono", + "hmac", + "log", + "rand", + "serde", + "serde_json", + "sha2", +] + +[[package]] +name = "async-std" +version = "1.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9f06685bad74e0570f5213741bea82158279a4103d988e57bfada11ad230341" +dependencies = [ + "async-attributes", + "async-channel", + "async-global-executor", + "async-io", + "async-lock", + "crossbeam-utils 0.8.5", + "futures-channel", + "futures-core", + "futures-io", + "futures-lite", + "gloo-timers", + "kv-log-macro", + "log", + "memchr", + "num_cpus", + "once_cell", + "pin-project-lite 0.2.7", + "pin-utils", + "slab", + "wasm-bindgen-futures", +] + +[[package]] +name = "async-task" +version = "4.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e91831deabf0d6d7ec49552e489aed63b7456a7a3c46cff62adad428110b0af0" + +[[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 = "atomic-waker" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "065374052e7df7ee4047b1160cca5e1467a12351a40b3da123c870ba0b8eda2a" + +[[package]] +name = "autocfg" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cdb031dd78e28731d87d56cc8ffef4a8f36ca26c38fe2de700543e627f8a464a" + +[[package]] +name = "axum" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3ecc2fad7de703d3117d448ca0e0bf45932510b323754b0a848221aed5647c7c" +dependencies = [ + "async-trait", + "bytes 1.0.1", + "futures-util", + "headers", + "http", + "http-body", + "hyper", + "pin-project", + "regex", + "serde", + "serde_json", + "serde_urlencoded", + "tokio 1.9.0", + "tokio-util 0.6.7", + "tower", + "tower-http", +] + +[[package]] +name = "backend" +version = "0.1.0" +dependencies = [ + "async-lock", + "async-redis-session", + "async-session", + "async-std", + "axum", + "chrono", + "diesel", + "diesel_migrations", + "dotenv", + "fern", + "headers", + "http", + "hyper", + "log", + "oauth2", + "redis 0.21.0", + "reqwest", + "serde", + "serde-redis", + "serde_json", + "tokio 1.9.0", + "tokio-diesel", + "tower", + "url", + "uuid", +] + +[[package]] +name = "base64" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b25d992356d2eb0ed82172f5248873db5560c4721f564b13cb5193bda5e668e" +dependencies = [ + "byteorder", +] + +[[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", +] + +[[package]] +name = "bitflags" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf1de2fe8c75bc145a2f577add951f8134889b4795d47466a54a5c846d691693" + +[[package]] +name = "blake3" +version = "0.3.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b64485778c4f16a6a5a9d335e80d449ac6c70cdd6a06d2af18a6f6f775a125b3" +dependencies = [ + "arrayref", + "arrayvec", + "cc", + "cfg-if 0.1.10", + "constant_time_eq", + "crypto-mac 0.8.0", + "digest", +] + +[[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 = "blocking" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c5e170dbede1f740736619b776d7251cb1b9095c435c34d8ca9f57fcd2f335e9" +dependencies = [ + "async-channel", + "async-task", + "atomic-waker", + "fastrand", + "futures-lite", + "once_cell", +] + +[[package]] +name = "bumpalo" +version = "3.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c59e7af012c713f529e7a3ee57ce9b31ddd858d4b512923602f74608b009631" + +[[package]] +name = "byteorder" +version = "1.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "14c189c53d098945499cdfa7ecc63567cf3886b3332b312a5b4585d8d3a6a610" + +[[package]] +name = "bytes" +version = "0.5.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0e4cec68f03f32e44924783795810fa50a7035d8c8ebe78580ad7e6c703fba38" + +[[package]] +name = "bytes" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b700ce4376041dcd0a327fd0097c41095743c4c8af8887265942faf1100bd040" + +[[package]] +name = "cache-padded" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "631ae5198c9be5e753e5cc215e1bd73c2b466a3565173db433f52bb9d3e66dba" + +[[package]] +name = "cc" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e70cc2f62c6ce1868963827bd677764c62d07c3d9a3e1fb1177ee1a9ab199eb2" + +[[package]] +name = "cfg-if" +version = "0.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4785bdd1c96b2a846b2bd7cc02e86b6b3dbf14e7e53446c4f54c92a361040822" + +[[package]] +name = "cfg-if" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" + +[[package]] +name = "chrono" +version = "0.4.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "670ad68c9088c2a963aaa298cb369688cf3f9465ce5e2d4ca10e6e0098a1ce73" +dependencies = [ + "libc", + "num-integer", + "num-traits", + "serde", + "time", + "winapi 0.3.9", +] + +[[package]] +name = "combine" +version = "4.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2d47c1b11006b87e492b53b313bb699ce60e16613c4dddaa91f8f7c220ab2fa" +dependencies = [ + "bytes 0.5.6", + "bytes 1.0.1", + "futures-util", + "memchr", + "pin-project-lite 0.2.7", + "tokio 0.2.25", + "tokio 1.9.0", +] + +[[package]] +name = "concurrent-queue" +version = "1.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "30ed07550be01594c6026cff2a1d7fe9c8f683caa798e12b68694ac9e88286a3" +dependencies = [ + "cache-padded", +] + +[[package]] +name = "constant_time_eq" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "245097e9a4535ee1e3e3931fcfcd55a796a44c643e8596ff6566d68f09b87bbc" + +[[package]] +name = "core-foundation" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0a89e2ae426ea83155dccf10c0fa6b1463ef6d5fcb44cee0b224a408fa640a62" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "core-foundation-sys" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ea221b5284a47e40033bf9b66f35f984ec0ea2931eb03505246cd27a963f981b" + +[[package]] +name = "cpufeatures" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "66c99696f6c9dd7f35d486b9d04d7e6e202aa3e8c40d553f2fdf5e7e0c6a71ef" +dependencies = [ + "libc", +] + +[[package]] +name = "crc32fast" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "81156fece84ab6a9f2afdb109ce3ae577e42b1228441eded99bd77f627953b1a" +dependencies = [ + "cfg-if 1.0.0", +] + +[[package]] +name = "crossbeam-channel" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8ec7fcd21571dc78f96cc96243cab8d8f035247c3efd16c687be154c3fa9efa" +dependencies = [ + "crossbeam-utils 0.6.6", +] + +[[package]] +name = "crossbeam-utils" +version = "0.6.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "04973fa96e96579258a5091af6003abde64af786b860f18622b82e026cca60e6" +dependencies = [ + "cfg-if 0.1.10", + "lazy_static", +] + +[[package]] +name = "crossbeam-utils" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d82cfc11ce7f2c3faef78d8a684447b40d503d9681acebed6cb728d45940c4db" +dependencies = [ + "cfg-if 1.0.0", + "lazy_static", +] + +[[package]] +name = "crypto-mac" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b584a330336237c1eecd3e94266efb216c56ed91225d634cb2991c5f3fd1aeab" +dependencies = [ + "generic-array", + "subtle", +] + +[[package]] +name = "crypto-mac" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b1d1a86f49236c215f271d40892d5fc950490551400b02ef360692c29815c714" +dependencies = [ + "generic-array", + "subtle", +] + +[[package]] +name = "ctor" +version = "0.1.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e98e2ad1a782e33928b96fc3948e7c355e5af34ba4de7670fe8bac2a3b2006d" +dependencies = [ + "quote", + "syn", +] + +[[package]] +name = "diesel" +version = "1.4.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bba51ca66f57261fd17cadf8b73e4775cc307d0521d855de3f5de91a8f074e0e" +dependencies = [ + "bitflags", + "byteorder", + "chrono", + "diesel_derives", + "pq-sys", + "r2d2", + "serde_json", + "uuid", +] + +[[package]] +name = "diesel_derives" +version = "1.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "45f5098f628d02a7a0f68ddba586fb61e80edec3bdc1be3b921f4ceec60858d3" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "diesel_migrations" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bf3cde8413353dc7f5d72fa8ce0b99a560a359d2c5ef1e5817ca731cd9008f4c" +dependencies = [ + "migrations_internals", + "migrations_macros", +] + +[[package]] +name = "digest" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3dd60d1080a57a05ab032377049e0591415d2b31afd7028356dbf3cc6dcb066" +dependencies = [ + "generic-array", +] + +[[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 = "encoding_rs" +version = "0.8.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "80df024fbc5ac80f87dfef0d9f5209a252f2a497f7f42944cff24d8253cac065" +dependencies = [ + "cfg-if 1.0.0", +] + +[[package]] +name = "event-listener" +version = "2.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f7531096570974c3a9dcf9e4b8e1cede1ec26cf5046219fb3b9d897503b9be59" + +[[package]] +name = "fastrand" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b394ed3d285a429378d3b384b9eb1285267e7df4b166df24b7a6939a04dc392e" +dependencies = [ + "instant", +] + +[[package]] +name = "fern" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8c9a4820f0ccc8a7afd67c39a0f1a0f4b07ca1725164271a64939d7aeb9af065" +dependencies = [ + "log", +] + +[[package]] +name = "flate2" +version = "1.0.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cd3aec53de10fe96d7d8c565eb17f2c687bb5518a2ec453b5b1252964526abe0" +dependencies = [ + "cfg-if 1.0.0", + "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 = "fuchsia-zircon" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2e9763c69ebaae630ba35f74888db465e49e259ba1bc0eda7d06f4a067615d82" +dependencies = [ + "bitflags", + "fuchsia-zircon-sys", +] + +[[package]] +name = "fuchsia-zircon-sys" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3dcaa9ae7725d12cdb85b3ad99a434db70b468c09ded17e012d86b5c1010f7a7" + +[[package]] +name = "futures" +version = "0.3.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1adc00f486adfc9ce99f77d717836f0c5aa84965eb0b4f051f4e83f7cab53f8b" +dependencies = [ + "futures-channel", + "futures-core", + "futures-io", + "futures-sink", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-channel" +version = "0.3.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "74ed2411805f6e4e3d9bc904c95d5d423b89b3b25dc0250aa74729de20629ff9" +dependencies = [ + "futures-core", + "futures-sink", +] + +[[package]] +name = "futures-core" +version = "0.3.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "af51b1b4a7fdff033703db39de8802c673eb91855f2e0d47dcf3bf2c0ef01f99" + +[[package]] +name = "futures-io" +version = "0.3.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b0e06c393068f3a6ef246c75cdca793d6a46347e75286933e5e75fd2fd11582" + +[[package]] +name = "futures-lite" +version = "1.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7694489acd39452c77daa48516b894c153f192c3578d5a839b62c58099fcbf48" +dependencies = [ + "fastrand", + "futures-core", + "futures-io", + "memchr", + "parking", + "pin-project-lite 0.2.7", + "waker-fn", +] + +[[package]] +name = "futures-macro" +version = "0.3.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c54913bae956fb8df7f4dc6fc90362aa72e69148e3f39041fbe8742d21e0ac57" +dependencies = [ + "autocfg", + "proc-macro-hack", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "futures-sink" +version = "0.3.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c0f30aaa67363d119812743aa5f33c201a7a66329f97d1a887022971feea4b53" + +[[package]] +name = "futures-task" +version = "0.3.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbe54a98670017f3be909561f6ad13e810d9a51f3f061b902062ca3da80799f2" + +[[package]] +name = "futures-util" +version = "0.3.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67eb846bfd58e44a8481a00049e82c43e0ccb5d61f8dc071057cb19249dd4d78" +dependencies = [ + "autocfg", + "futures-core", + "futures-io", + "futures-macro", + "futures-sink", + "futures-task", + "memchr", + "pin-project-lite 0.2.7", + "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 0.9.3", +] + +[[package]] +name = "getrandom" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7fcd999463524c52659517fe2cea98493cfe485d10565e7b0fb07dbba7ad2753" +dependencies = [ + "cfg-if 1.0.0", + "js-sys", + "libc", + "wasi", + "wasm-bindgen", +] + +[[package]] +name = "gloo-timers" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47204a46aaff920a1ea58b11d03dec6f704287d27561724a4631e450654a891f" +dependencies = [ + "futures-channel", + "futures-core", + "js-sys", + "wasm-bindgen", + "web-sys", +] + +[[package]] +name = "h2" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "825343c4eef0b63f541f8903f395dc5beb362a979b5799a84062527ef1e37726" +dependencies = [ + "bytes 1.0.1", + "fnv", + "futures-core", + "futures-sink", + "futures-util", + "http", + "indexmap", + "slab", + "tokio 1.9.0", + "tokio-util 0.6.7", + "tracing", +] + +[[package]] +name = "hashbrown" +version = "0.11.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ab5ef0d4909ef3724cc8cce6ccc8572c5c817592e9285f5464f8e86f8bd3726e" + +[[package]] +name = "hdrhistogram" +version = "6.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08d331ebcdbca4acbefe5da8c3299b2e246f198a8294cc5163354e743398b89d" +dependencies = [ + "base64 0.10.1", + "byteorder", + "crossbeam-channel", + "flate2", + "nom", + "num-traits", +] + +[[package]] +name = "headers" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0b7591fb62902706ae8e7aaff416b1b0fa2c0fd0878b46dc13baa3712d8a855" +dependencies = [ + "base64 0.13.0", + "bitflags", + "bytes 1.0.1", + "headers-core", + "http", + "mime", + "sha-1", + "time", +] + +[[package]] +name = "headers-core" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7f66481bfee273957b1f20485a4ff3362987f85b2c236580d81b4eb7a326429" +dependencies = [ + "http", +] + +[[package]] +name = "hermit-abi" +version = "0.1.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "62b467343b94ba476dcb2500d242dadbb39557df889310ac77c5d99100aaac33" +dependencies = [ + "libc", +] + +[[package]] +name = "hmac" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2a2a2320eb7ec0ebe8da8f744d7812d9fc4cb4d09344ac01898dbcb6a20ae69b" +dependencies = [ + "crypto-mac 0.11.1", + "digest", +] + +[[package]] +name = "http" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "527e8c9ac747e28542699a951517aa9a6945af506cd1f2e1b53a576c17b6cc11" +dependencies = [ + "bytes 1.0.1", + "fnv", + "itoa", +] + +[[package]] +name = "http-body" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "60daa14be0e0786db0f03a9e57cb404c9d756eed2b6c62b9ea98ec5743ec75a9" +dependencies = [ + "bytes 1.0.1", + "http", + "pin-project-lite 0.2.7", +] + +[[package]] +name = "httparse" +version = "1.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f3a87b616e37e93c22fb19bcd386f02f3af5ea98a25670ad0fce773de23c5e68" + +[[package]] +name = "httpdate" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6456b8a6c8f33fee7d958fcd1b60d55b11940a79e63ae87013e6d22e26034440" + +[[package]] +name = "hyper" +version = "0.14.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b61cf2d1aebcf6e6352c97b81dc2244ca29194be1b276f5d8ad5c6330fffb11" +dependencies = [ + "bytes 1.0.1", + "futures-channel", + "futures-core", + "futures-util", + "h2", + "http", + "http-body", + "httparse", + "httpdate", + "itoa", + "pin-project-lite 0.2.7", + "socket2", + "tokio 1.9.0", + "tower-service", + "tracing", + "want", +] + +[[package]] +name = "hyper-rustls" +version = "0.22.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5f9f7a97316d44c0af9b0301e65010573a853a9fc97046d7331d7f6bc0fd5a64" +dependencies = [ + "futures-util", + "hyper", + "log", + "rustls", + "tokio 1.9.0", + "tokio-rustls", + "webpki", +] + +[[package]] +name = "hyper-tls" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d6183ddfa99b85da61a140bea0efc93fdf56ceaa041b37d553518030827f9905" +dependencies = [ + "bytes 1.0.1", + "hyper", + "native-tls", + "tokio 1.9.0", + "tokio-native-tls", +] + +[[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 = "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.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bee0328b1209d157ef001c94dd85b4f8f64139adb0eac2659f4b08382b2f474d" +dependencies = [ + "cfg-if 1.0.0", +] + +[[package]] +name = "iovec" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b2b3ea6ff95e175473f8ffe6a7eb7c00d054240321b84c57051175fe3c1e075e" +dependencies = [ + "libc", +] + +[[package]] +name = "ipnet" +version = "2.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68f2d64f2edebec4ce84ad108148e67e1064789bee435edc5b60ad398714a3a9" + +[[package]] +name = "itoa" +version = "0.4.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dd25036021b0de88a0aff6b850051563c6516d0bf53f8638938edbb9de732736" + +[[package]] +name = "js-sys" +version = "0.3.52" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ce791b7ca6638aae45be056e068fc756d871eb3b3b10b8efa62d1c9cec616752" +dependencies = [ + "wasm-bindgen", +] + +[[package]] +name = "kernel32-sys" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7507624b29483431c0ba2d82aece8ca6cdba9382bff4ddd0f7490560c056098d" +dependencies = [ + "winapi 0.2.8", + "winapi-build", +] + +[[package]] +name = "kv-log-macro" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0de8b303297635ad57c9f5059fd9cee7a47f8e8daa09df0fcd07dd39fb22977f" +dependencies = [ + "log", +] + +[[package]] +name = "lazy_static" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" + +[[package]] +name = "libc" +version = "0.2.98" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "320cfe77175da3a483efed4bc0adc1968ca050b098ce4f2f1c13a56626128790" + +[[package]] +name = "lock_api" +version = "0.4.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0382880606dff6d15c9476c416d18690b72742aa7b605bb6dd6ec9030fbf07eb" +dependencies = [ + "scopeguard", +] + +[[package]] +name = "log" +version = "0.4.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "51b9bbe6c47d51fc3e1a9b945965946b4c44142ab8792c50835a980d362c2710" +dependencies = [ + "cfg-if 1.0.0", + "value-bag", +] + +[[package]] +name = "matches" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7ffc5c5338469d4d3ea17d269fa8ea3512ad247247c30bd2df69e68309ed0a08" + +[[package]] +name = "memchr" +version = "2.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b16bd47d9e329435e309c58469fe0791c2d0d1ba96ec0954152a5ae2b04387dc" + +[[package]] +name = "migrations_internals" +version = "1.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b4fc84e4af020b837029e017966f86a1c2d5e83e64b589963d5047525995860" +dependencies = [ + "diesel", +] + +[[package]] +name = "migrations_macros" +version = "1.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9753f12909fd8d923f75ae5c3258cae1ed3c8ec052e1b38c93c21a6d157f789c" +dependencies = [ + "migrations_internals", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "mime" +version = "0.3.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2a60c7ce501c71e03a9c9c0d35b861413ae925bd979cc7a4e30d060069aaac8d" + +[[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.6.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4afd66f5b91bf2a3bc13fad0e21caedac168ca4c707504e75585648ae80e4cc4" +dependencies = [ + "cfg-if 0.1.10", + "fuchsia-zircon", + "fuchsia-zircon-sys", + "iovec", + "kernel32-sys", + "libc", + "log", + "miow 0.2.2", + "net2", + "slab", + "winapi 0.2.8", +] + +[[package]] +name = "mio" +version = "0.7.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8c2bdb6314ec10835cd3293dd268473a835c02b7b352e788be788b3c6ca6bb16" +dependencies = [ + "libc", + "log", + "miow 0.3.7", + "ntapi", + "winapi 0.3.9", +] + +[[package]] +name = "mio-uds" +version = "0.6.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "afcb699eb26d4332647cc848492bbc15eafb26f08d0304550d5aa1f612e066f0" +dependencies = [ + "iovec", + "libc", + "mio 0.6.23", +] + +[[package]] +name = "miow" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebd808424166322d4a38da87083bfddd3ac4c131334ed55856112eb06d46944d" +dependencies = [ + "kernel32-sys", + "net2", + "winapi 0.2.8", + "ws2_32-sys", +] + +[[package]] +name = "miow" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9f1c5b025cda876f66ef43a113f91ebc9f4ccef34843000e0adf6ebbab84e21" +dependencies = [ + "winapi 0.3.9", +] + +[[package]] +name = "native-tls" +version = "0.2.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "48ba9f7719b5a0f42f338907614285fb5fd70e53858141f69898a1fb7203b24d" +dependencies = [ + "lazy_static", + "libc", + "log", + "openssl", + "openssl-probe", + "openssl-sys", + "schannel", + "security-framework", + "security-framework-sys", + "tempfile", +] + +[[package]] +name = "net2" +version = "0.2.37" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "391630d12b68002ae1e25e8f974306474966550ad82dac6886fb8910c19568ae" +dependencies = [ + "cfg-if 0.1.10", + "libc", + "winapi 0.3.9", +] + +[[package]] +name = "nom" +version = "4.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2ad2a91a8e869eeb30b9cb3119ae87773a8f4ae617f41b1eb9c154b2905f7bd6" +dependencies = [ + "memchr", + "version_check 0.1.5", +] + +[[package]] +name = "ntapi" +version = "0.3.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f6bb902e437b6d86e03cce10a7e2af662292c5dfef23b65899ea3ac9354ad44" +dependencies = [ + "winapi 0.3.9", +] + +[[package]] +name = "num-integer" +version = "0.1.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d2cc698a63b549a70bc047073d2949cce27cd1c7b0a4a862d08a8031bc2801db" +dependencies = [ + "autocfg", + "num-traits", +] + +[[package]] +name = "num-traits" +version = "0.2.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a64b1ec5cda2586e284722486d802acf1f7dbdc623e2bfc57e65ca1cd099290" +dependencies = [ + "autocfg", +] + +[[package]] +name = "num_cpus" +version = "1.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05499f3756671c15885fee9034446956fff3f243d6077b91e5767df161f766b3" +dependencies = [ + "hermit-abi", + "libc", +] + +[[package]] +name = "oauth2" +version = "4.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "80e47cfc4c0a1a519d9a025ebfbac3a2439d1b5cdf397d72dcb79b11d9920dab" +dependencies = [ + "base64 0.13.0", + "chrono", + "getrandom", + "http", + "rand", + "reqwest", + "serde", + "serde_json", + "serde_path_to_error", + "sha2", + "thiserror", + "url", +] + +[[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.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "549430950c79ae24e6d02e0b7404534ecf311d94cc9f861e9e4020187d13d885" +dependencies = [ + "bitflags", + "cfg-if 1.0.0", + "foreign-types", + "libc", + "once_cell", + "openssl-sys", +] + +[[package]] +name = "openssl-probe" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "28988d872ab76095a6e6ac88d99b54fd267702734fd7ffe610ca27f533ddb95a" + +[[package]] +name = "openssl-sys" +version = "0.9.65" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a7907e3bfa08bb85105209cdfcb6c63d109f8f6c1ed6ca318fff5c1853fbc1d" +dependencies = [ + "autocfg", + "cc", + "libc", + "pkg-config", + "vcpkg", +] + +[[package]] +name = "parking" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "427c3892f9e783d91cc128285287e70a59e206ca452770ece88a76f7a3eddd72" + +[[package]] +name = "parking_lot" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6d7744ac029df22dca6284efe4e898991d28e3085c706c972bcd7da4a27a15eb" +dependencies = [ + "instant", + "lock_api", + "parking_lot_core", +] + +[[package]] +name = "parking_lot_core" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fa7a782938e745763fe6907fc6ba86946d72f49fe7e21de074e08128a99fb018" +dependencies = [ + "cfg-if 1.0.0", + "instant", + "libc", + "redox_syscall", + "smallvec", + "winapi 0.3.9", +] + +[[package]] +name = "percent-encoding" +version = "2.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d4fd5641d01c8f18a23da7b6fe29298ff4b55afcccdf78973b24cf3175fee32e" + +[[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.1.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "257b64915a082f7811703966789728173279bdebb956b143dbcd23f6f970a777" + +[[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.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3831453b3449ceb48b6d9c7ad7c96d5ea673e9b470a1dc578c2ce6521230884c" + +[[package]] +name = "polling" +version = "2.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92341d779fa34ea8437ef4d82d440d5e1ce3f3ff7f824aa64424cd481f9a1f25" +dependencies = [ + "cfg-if 1.0.0", + "libc", + "log", + "wepoll-ffi", + "winapi 0.3.9", +] + +[[package]] +name = "ppv-lite86" +version = "0.2.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac74c624d6b2d21f425f752262f42188365d7b8ff1aff74c82e45136510a4857" + +[[package]] +name = "pq-sys" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ac25eee5a0582f45a67e837e350d784e7003bd29a5f460796772061ca49ffda" +dependencies = [ + "vcpkg", +] + +[[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.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c7ed8b8c7b886ea3ed7dde405212185f423ab44682667c8c6dd14aa1d9f6612" +dependencies = [ + "unicode-xid", +] + +[[package]] +name = "quote" +version = "1.0.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3d0b9745dc2debf507c8422de05d7226cc1f0644216dfdfead988f9b1ab32a7" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "r2d2" +version = "0.8.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "545c5bc2b880973c9c10e4067418407a0ccaa3091781d1671d46eb35107cb26f" +dependencies = [ + "log", + "parking_lot", + "scheduled-thread-pool", +] + +[[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.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "95357caf2640abc54651b93c98a8df4fe1ccbf44b8e601ccdf43d5c1451f29ac" +dependencies = [ + "async-std", + "async-trait", + "bytes 0.5.6", + "combine", + "dtoa", + "futures-util", + "itoa", + "percent-encoding", + "pin-project-lite 0.1.12", + "sha1", + "tokio 0.2.25", + "tokio-util 0.3.1", + "url", +] + +[[package]] +name = "redis" +version = "0.21.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3bbc1838d8d0b423f325d6fac80c5f19109c7d16c8c37c584893dc17cf71c63d" +dependencies = [ + "async-std", + "async-trait", + "bytes 1.0.1", + "combine", + "dtoa", + "futures-util", + "itoa", + "percent-encoding", + "pin-project-lite 0.2.7", + "sha1", + "tokio 1.9.0", + "tokio-util 0.6.7", + "url", +] + +[[package]] +name = "redox_syscall" +version = "0.2.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5ab49abadf3f9e1c4bc499e8845e152ad87d2ad2d30371841171169e9d75feee" +dependencies = [ + "bitflags", +] + +[[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 = "remove_dir_all" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3acd125665422973a33ac9d3dd2df85edad0f4ae9b00dafb1a05e43a9f5ef8e7" +dependencies = [ + "winapi 0.3.9", +] + +[[package]] +name = "reqwest" +version = "0.11.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "246e9f61b9bb77df069a947682be06e31ac43ea37862e244a69f177694ea6d22" +dependencies = [ + "base64 0.13.0", + "bytes 1.0.1", + "encoding_rs", + "futures-core", + "futures-util", + "http", + "http-body", + "hyper", + "hyper-rustls", + "hyper-tls", + "ipnet", + "js-sys", + "lazy_static", + "log", + "mime", + "native-tls", + "percent-encoding", + "pin-project-lite 0.2.7", + "rustls", + "serde", + "serde_json", + "serde_urlencoded", + "tokio 1.9.0", + "tokio-native-tls", + "tokio-rustls", + "url", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", + "webpki-roots", + "winreg", +] + +[[package]] +name = "ring" +version = "0.16.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3053cf52e236a3ed746dfc745aa9cacf1b791d846bdaf412f60a8d7d6e17c8fc" +dependencies = [ + "cc", + "libc", + "once_cell", + "spin", + "untrusted", + "web-sys", + "winapi 0.3.9", +] + +[[package]] +name = "rustls" +version = "0.19.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "35edb675feee39aec9c99fa5ff985081995a06d594114ae14cbe797ad7b7a6d7" +dependencies = [ + "base64 0.13.0", + "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 = "schannel" +version = "0.1.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f05ba609c234e60bee0d547fe94a4c7e9da733d1c962cf6e59efa4cd9c8bc75" +dependencies = [ + "lazy_static", + "winapi 0.3.9", +] + +[[package]] +name = "scheduled-thread-pool" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc6f74fd1204073fa02d5d5d68bec8021be4c38690b61264b2fdb48083d0e7d7" +dependencies = [ + "parking_lot", +] + +[[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 = "security-framework" +version = "2.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "23a2ac85147a3a11d77ecf1bc7166ec0b92febfa4461c37944e180f319ece467" +dependencies = [ + "bitflags", + "core-foundation", + "core-foundation-sys", + "libc", + "security-framework-sys", +] + +[[package]] +name = "security-framework-sys" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7e4effb91b4b8b6fb7732e670b6cee160278ff8e6bf485c7805d9e319d76e284" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "serde" +version = "1.0.127" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f03b9878abf6d14e6779d3f24f07b2cfa90352cfec4acc5aab8f1ac7f146fae8" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde-redis" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "23b3615e7706775f0253ee23478acc7678cd3d4cdaf72509426797bc8358be06" +dependencies = [ + "redis 0.17.0", + "serde", +] + +[[package]] +name = "serde_derive" +version = "1.0.127" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a024926d3432516606328597e0f224a51355a493b49fdd67e9209187cbe55ecc" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "serde_json" +version = "1.0.66" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "336b10da19a12ad094b59d870ebde26a45402e5b470add4b5fd03c5048a32127" +dependencies = [ + "itoa", + "ryu", + "serde", +] + +[[package]] +name = "serde_path_to_error" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42f6109f0506e20f7e0f910e51a0079acf41da8e0694e6442527c4ddf5a2b158" +dependencies = [ + "serde", +] + +[[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", +] + +[[package]] +name = "sha-1" +version = "0.9.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a0c8611594e2ab4ebbf06ec7cbbf0a99450b8570e96cbf5188b5d5f6ef18d81" +dependencies = [ + "block-buffer", + "cfg-if 1.0.0", + "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.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b362ae5752fd2137731f9fa25fd4d9058af34666ca1966fb969119cc35719f12" +dependencies = [ + "block-buffer", + "cfg-if 1.0.0", + "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.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f173ac3d1a7e3b28003f40de0b5ce7fe2710f9b9dc3fc38664cebee46b3b6527" + +[[package]] +name = "smallvec" +version = "1.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fe0f37c9e8f3c5a4a66ad655a93c74daac4ad00c441533bf5c6e7990bb42604e" + +[[package]] +name = "socket2" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "765f090f0e423d2b55843402a07915add955e7d60657db13707a159727326cad" +dependencies = [ + "libc", + "winapi 0.3.9", +] + +[[package]] +name = "spin" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e63cff320ae2c57904679ba7cb63280a3dc4613885beafb148ee7bf9aa9042d" + +[[package]] +name = "subtle" +version = "2.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6bdef32e8150c2a081110b42772ffe7d7c9032b606bc226c8260fd97e0976601" + +[[package]] +name = "syn" +version = "1.0.74" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1873d832550d4588c3dbc20f01361ab00bfe741048f71e3fecf145a7cc18b29c" +dependencies = [ + "proc-macro2", + "quote", + "unicode-xid", +] + +[[package]] +name = "tempfile" +version = "3.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dac1c663cfc93810f88aed9b8941d48cabf856a1b111c29a40439018d870eb22" +dependencies = [ + "cfg-if 1.0.0", + "libc", + "rand", + "redox_syscall", + "remove_dir_all", + "winapi 0.3.9", +] + +[[package]] +name = "thiserror" +version = "1.0.26" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93119e4feac1cbe6c798c34d3a53ea0026b0b1de6a120deef895137c0529bfe2" +dependencies = [ + "thiserror-impl", +] + +[[package]] +name = "thiserror-impl" +version = "1.0.26" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "060d69a0afe7796bf42e9e2ff91f5ee691fb15c53d38b4b62a9a53eb23164745" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "time" +version = "0.1.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6db9e6914ab8b1ae1c260a4ae7a49b6c5611b40328a735b21862567685e73255" +dependencies = [ + "libc", + "wasi", + "winapi 0.3.9", +] + +[[package]] +name = "tinyvec" +version = "1.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "848a1e1181b9f6753b5e96a092749e29b11d19ede67dfbbd6c7dc7e0f49b5338" +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 = "0.2.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6703a273949a90131b290be1fe7b039d0fc884aa1935860dfcbe056f28cd8092" +dependencies = [ + "bytes 0.5.6", + "fnv", + "futures-core", + "iovec", + "lazy_static", + "libc", + "memchr", + "mio 0.6.23", + "mio-uds", + "pin-project-lite 0.1.12", +] + +[[package]] +name = "tokio" +version = "1.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4b7b349f11a7047e6d1276853e612d152f5e8a352c61917887cc2169e2366b4c" +dependencies = [ + "autocfg", + "bytes 1.0.1", + "libc", + "memchr", + "mio 0.7.13", + "num_cpus", + "once_cell", + "parking_lot", + "pin-project-lite 0.2.7", + "signal-hook-registry", + "tokio-macros", + "winapi 0.3.9", +] + +[[package]] +name = "tokio-diesel" +version = "0.3.0" +source = "git+https://github.com/mehcode/tokio-diesel#f4af42558246ab323600622ba8d08803d3c18842" +dependencies = [ + "async-trait", + "diesel", + "futures", + "r2d2", + "tokio 1.9.0", +] + +[[package]] +name = "tokio-macros" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "54473be61f4ebe4efd09cec9bd5d16fa51d70ea0192213d754d2d500457db110" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "tokio-native-tls" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f7d995660bd2b7f8c1568414c1126076c13fbb725c40112dc0120b78eb9b717b" +dependencies = [ + "native-tls", + "tokio 1.9.0", +] + +[[package]] +name = "tokio-rustls" +version = "0.22.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bc6844de72e57df1980054b38be3a9f4702aba4858be64dd700181a8a6d0e1b6" +dependencies = [ + "rustls", + "tokio 1.9.0", + "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 0.2.7", + "tokio 1.9.0", +] + +[[package]] +name = "tokio-util" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "be8242891f2b6cbef26a2d7e8605133c2c554cd35b3e4948ea892d6d68436499" +dependencies = [ + "bytes 0.5.6", + "futures-core", + "futures-sink", + "log", + "pin-project-lite 0.1.12", + "tokio 0.2.25", +] + +[[package]] +name = "tokio-util" +version = "0.6.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1caa0b0c8d94a049db56b5acf8cba99dc0623aab1b26d5b5f5e2d945846b3592" +dependencies = [ + "bytes 1.0.1", + "futures-core", + "futures-sink", + "log", + "pin-project-lite 0.2.7", + "tokio 1.9.0", +] + +[[package]] +name = "tower" +version = "0.4.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f60422bc7fefa2f3ec70359b8ff1caff59d785877eb70595904605bcc412470f" +dependencies = [ + "futures-core", + "futures-util", + "hdrhistogram", + "indexmap", + "pin-project", + "rand", + "slab", + "tokio 1.9.0", + "tokio-stream", + "tokio-util 0.6.7", + "tower-layer", + "tower-service", + "tracing", +] + +[[package]] +name = "tower-http" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b7b56efe69aa0ad2b5da6b942e57ea9f6fe683b7a314d4ff48662e2c8838de1" +dependencies = [ + "bytes 1.0.1", + "futures-core", + "futures-util", + "http", + "http-body", + "pin-project", + "tower-layer", + "tower-service", +] + +[[package]] +name = "tower-layer" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "343bc9466d3fe6b0f960ef45960509f84480bf4fd96f92901afe7ff3df9d3a62" + +[[package]] +name = "tower-service" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "360dfd1d6d30e05fda32ace2c8c70e9c0a9da713275777f5a4dbb8a1893930c6" + +[[package]] +name = "tracing" +version = "0.1.26" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09adeb8c97449311ccd28a427f96fb563e7fd31aabf994189879d9da2394b89d" +dependencies = [ + "cfg-if 1.0.0", + "log", + "pin-project-lite 0.2.7", + "tracing-attributes", + "tracing-core", +] + +[[package]] +name = "tracing-attributes" +version = "0.1.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c42e6fa53307c8a17e4ccd4dc81cf5ec38db9209f59b222210375b54ee40d1e2" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "tracing-core" +version = "0.1.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a9ff14f98b1a4b289c6248a023c1c2fa1491062964e9fed67ab29c4e4da4a052" +dependencies = [ + "lazy_static", +] + +[[package]] +name = "try-lock" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "59547bce71d9c38b83d9c0e92b6066c4253371f15005def0c30d9657f50c7642" + +[[package]] +name = "typenum" +version = "1.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "879f6906492a7cd215bfa4cf595b600146ccfac0c79bcbd1f3000162af5e8b06" + +[[package]] +name = "unicode-bidi" +version = "0.3.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eeb8be209bb1c96b7c177c7420d26e04eccacb0eeae6b980e35fcb74678107e0" +dependencies = [ + "matches", +] + +[[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-xid" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ccb82d61f80a663efe1f787a51b16b5a51e3314d6ac365b08639f52387b33f3" + +[[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", + "serde", +] + +[[package]] +name = "uuid" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bc5cf98d8186244414c848017f0e2676b3fcb46807f6668a97dfe67359a3c4b7" +dependencies = [ + "getrandom", + "serde", +] + +[[package]] +name = "value-bag" +version = "1.0.0-alpha.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dd320e1520f94261153e96f7534476ad869c14022aee1e59af7c778075d840ae" +dependencies = [ + "ctor", + "version_check 0.9.3", +] + +[[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.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "914b1a6776c4c929a602fafd8bc742e06365d4bcbe48c30f9cca5824f70dc9dd" + +[[package]] +name = "version_check" +version = "0.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5fecdca9a5291cc2b8dcf7dc02453fee791a280f3743cb0905f8822ae463b3fe" + +[[package]] +name = "waker-fn" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d5b2c62b4012a3e1eca5a7e077d13b3bf498c4073e33ccd58626607748ceeca" + +[[package]] +name = "want" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ce8a968cb1cd110d136ff8b819a556d6fb6d919363c61534f6860c7eb172ba0" +dependencies = [ + "log", + "try-lock", +] + +[[package]] +name = "wasi" +version = "0.10.0+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a143597ca7c7793eff794def352d41792a93c481eb1042423ff7ff72ba2c31f" + +[[package]] +name = "wasm-bindgen" +version = "0.2.75" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b608ecc8f4198fe8680e2ed18eccab5f0cd4caaf3d83516fa5fb2e927fda2586" +dependencies = [ + "cfg-if 1.0.0", + "serde", + "serde_json", + "wasm-bindgen-macro", +] + +[[package]] +name = "wasm-bindgen-backend" +version = "0.2.75" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "580aa3a91a63d23aac5b6b267e2d13cb4f363e31dce6c352fca4752ae12e479f" +dependencies = [ + "bumpalo", + "lazy_static", + "log", + "proc-macro2", + "quote", + "syn", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-futures" +version = "0.4.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "16646b21c3add8e13fdb8f20172f8a28c3dbf62f45406bcff0233188226cfe0c" +dependencies = [ + "cfg-if 1.0.0", + "js-sys", + "wasm-bindgen", + "web-sys", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.75" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "171ebf0ed9e1458810dfcb31f2e766ad6b3a89dbda42d8901f2b268277e5f09c" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.75" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c2657dd393f03aa2a659c25c6ae18a13a4048cebd220e147933ea837efc589f" +dependencies = [ + "proc-macro2", + "quote", + "syn", + "wasm-bindgen-backend", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.75" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2e0c4a743a309662d45f4ede961d7afa4ba4131a59a639f29b0069c3798bbcc2" + +[[package]] +name = "web-sys" +version = "0.3.52" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "01c70a82d842c9979078c772d4a1344685045f1a5628f677c2b2eab4dd7d2696" +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 = "wepoll-ffi" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d743fdedc5c64377b5fc2bc036b01c7fd642205a0d96356034ae3404d49eb7fb" +dependencies = [ + "cc", +] + +[[package]] +name = "winapi" +version = "0.2.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "167dc9d6949a9b857f3451275e911c3f44255842c1f7a76f33c55103a909087a" + +[[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-build" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2d315eee3b34aca4797b2da6b13ed88266e6d612562a0c46390af8299fc699bc" + +[[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-x86_64-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" + +[[package]] +name = "winreg" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0120db82e8a1e0b9fb3345a539c478767c0048d842860994d96113d5b667bd69" +dependencies = [ + "winapi 0.3.9", +] + +[[package]] +name = "ws2_32-sys" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d59cefebd0c892fa2dd6de581e937301d8552cb44489cdff035c6187cb63fa5e" +dependencies = [ + "winapi 0.2.8", + "winapi-build", +] diff --git a/backend/Cargo.toml b/backend/Cargo.toml new file mode 100644 index 0000000..712764a --- /dev/null +++ b/backend/Cargo.toml @@ -0,0 +1,33 @@ +[package] +edition = "2018" +name = "backend" +version = "0.1.0" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +async-lock = "2.4.0" +async-redis-session = {git = "https://github.com/jbr/async-redis-session", version = "0.2.2"} +async-session = "3.0.0" +async-std = {version = "1.9.0", features = ["attributes"]} +axum = {version = "0.1.1", features = ["headers"]} +chrono = {version = "0.4.0", features = ["serde"]} +diesel = {version = "1.4.7", features = ["postgres", "chrono", "serde_json", "r2d2", "uuidv07"]} +diesel_migrations = {version = "1.4.0"} +dotenv = "0.15.0" +fern = "0.6.0" +headers = "0.3.4" +http = "0.2.4" +hyper = {version = "0.14.11", features = ["full"]} +log = "0.4.14" +oauth2 = "4.1.0" +redis = {version = "0.21.0", features = ["aio", "async-std-comp"]} +reqwest = {version = "0.11.4", features = ["json"]} +serde = {version = "1.0.127", features = ["derive"]} +serde-redis = "0.10.0" +serde_json = "1.0.66" +tokio = {version = "1.9.0", features = ["full"]} +tokio-diesel = {git = "https://github.com/mehcode/tokio-diesel", version = "0.3.0"} +tower = {version = "0.4.6", features = ["full"]} +url = "2.2.2" +uuid = {version = "0.8.2", features = ["serde", "v4"]} diff --git a/backend/README.md b/backend/README.md deleted file mode 100644 index 85dfcb7..0000000 --- a/backend/README.md +++ /dev/null @@ -1,2 +0,0 @@ -# todo - diff --git a/backend/config.json.example b/backend/config.json.example deleted file mode 100644 index 763b2b4..0000000 --- a/backend/config.json.example +++ /dev/null @@ -1,16 +0,0 @@ -{ - "secret": "TEST_SECRET", - "https": true, - "alter_db": true, - "port": 8080, - "frontend_url": "localhost:3000", - "db_url": "postgres://postgres:@127.0.0.1/todo", - "cert": "", - "cert_key": "", - "mail_host": "", - "mail_port": 465, - "mail_username": "", - "mail_password": "", - "discord_id": "", - "discord_secret": "" -} \ No newline at end of file diff --git a/backend/default.profraw b/backend/default.profraw new file mode 100644 index 0000000..e69de29 diff --git a/backend/diesel.toml b/backend/diesel.toml new file mode 100644 index 0000000..92267c8 --- /dev/null +++ b/backend/diesel.toml @@ -0,0 +1,5 @@ +# For documentation on how to configure this file, +# see diesel.rs/guides/configuring-diesel-cli + +[print_schema] +file = "src/schema.rs" diff --git a/backend/env.sh b/backend/env.sh new file mode 100755 index 0000000..70f747e --- /dev/null +++ b/backend/env.sh @@ -0,0 +1,26 @@ +#!/bin/bash + +args=("$@") + +if [[ ${1} == "prod" ]]; then + echo "prod build" + export RUSTFLAGS="" + ARGS="--release" + if [[ ${2} ]]; then + cargo ${2} $ARGS ${args[@]:2} + else + echo "defaulting to build" + cargo build $ARGS + fi +else + echo "dev build" + export RUSTFLAGS="-Zinstrument-coverage -Zmacro-backtrace" + ARGS="" + if [[ ${1} ]]; then + cargo ${1} $ARGS ${args[@]:1} + else + echo "defaulting to build+tests" + cargo test + grcov . --binary-path ./target/debug -s . -t html --branch --ignore-not-existing -o ./test_coverage/ + fi +fi \ No newline at end of file diff --git a/backend/migrations/.gitkeep b/backend/migrations/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/backend/migrations/00000000000000_diesel_initial_setup/down.sql b/backend/migrations/00000000000000_diesel_initial_setup/down.sql new file mode 100644 index 0000000..a9f5260 --- /dev/null +++ b/backend/migrations/00000000000000_diesel_initial_setup/down.sql @@ -0,0 +1,6 @@ +-- This file was automatically created by Diesel to setup helper functions +-- and other internal bookkeeping. This file is safe to edit, any future +-- changes will be added to existing projects as new migrations. + +DROP FUNCTION IF EXISTS diesel_manage_updated_at(_tbl regclass); +DROP FUNCTION IF EXISTS diesel_set_updated_at(); diff --git a/backend/migrations/00000000000000_diesel_initial_setup/up.sql b/backend/migrations/00000000000000_diesel_initial_setup/up.sql new file mode 100644 index 0000000..d68895b --- /dev/null +++ b/backend/migrations/00000000000000_diesel_initial_setup/up.sql @@ -0,0 +1,36 @@ +-- This file was automatically created by Diesel to setup helper functions +-- and other internal bookkeeping. This file is safe to edit, any future +-- changes will be added to existing projects as new migrations. + + + + +-- Sets up a trigger for the given table to automatically set a column called +-- `updated_at` whenever the row is modified (unless `updated_at` was included +-- in the modified columns) +-- +-- # Example +-- +-- ```sql +-- CREATE TABLE users (id SERIAL PRIMARY KEY, updated_at TIMESTAMP NOT NULL DEFAULT NOW()); +-- +-- SELECT diesel_manage_updated_at('users'); +-- ``` +CREATE OR REPLACE FUNCTION diesel_manage_updated_at(_tbl regclass) RETURNS VOID AS $$ +BEGIN + EXECUTE format('CREATE TRIGGER set_updated_at BEFORE UPDATE ON %s + FOR EACH ROW EXECUTE PROCEDURE diesel_set_updated_at()', _tbl); +END; +$$ LANGUAGE plpgsql; + +CREATE OR REPLACE FUNCTION diesel_set_updated_at() RETURNS trigger AS $$ +BEGIN + IF ( + NEW IS DISTINCT FROM OLD AND + NEW.updated_at IS NOT DISTINCT FROM OLD.updated_at + ) THEN + NEW.updated_at := current_timestamp; + END IF; + RETURN NEW; +END; +$$ LANGUAGE plpgsql; diff --git a/backend/migrations/2021-08-05-011028_create_user/down.sql b/backend/migrations/2021-08-05-011028_create_user/down.sql new file mode 100644 index 0000000..42ebb17 --- /dev/null +++ b/backend/migrations/2021-08-05-011028_create_user/down.sql @@ -0,0 +1,2 @@ +-- This file should undo anything in `up.sql` +DROP TABLE IF EXISTS users; \ No newline at end of file diff --git a/backend/migrations/2021-08-05-011028_create_user/up.sql b/backend/migrations/2021-08-05-011028_create_user/up.sql new file mode 100644 index 0000000..4f8e215 --- /dev/null +++ b/backend/migrations/2021-08-05-011028_create_user/up.sql @@ -0,0 +1,9 @@ +-- Your SQL goes here +CREATE TABLE IF NOT EXISTS users ( + id UUID DEFAULT gen_random_uuid() PRIMARY KEY, + discord_id VARCHAR(255) NOT NULL, + created_at TIMESTAMP DEFAULT NOW() NOT NULL, + updated_at TIMESTAMP DEFAULT NOW() NOT NULL +); + +SELECT diesel_manage_updated_at('users'); \ No newline at end of file diff --git a/backend/migrations/2021-08-06-173217_create_block/down.sql b/backend/migrations/2021-08-06-173217_create_block/down.sql new file mode 100644 index 0000000..274dd46 --- /dev/null +++ b/backend/migrations/2021-08-06-173217_create_block/down.sql @@ -0,0 +1,2 @@ +-- This file should undo anything in `up.sql` +DROP TABLE IF EXISTS blocks; \ No newline at end of file diff --git a/backend/migrations/2021-08-06-173217_create_block/up.sql b/backend/migrations/2021-08-06-173217_create_block/up.sql new file mode 100644 index 0000000..b9b7ed9 --- /dev/null +++ b/backend/migrations/2021-08-06-173217_create_block/up.sql @@ -0,0 +1,12 @@ +-- Your SQL goes here +CREATE TABLE IF NOT EXISTS blocks ( + id UUID DEFAULT gen_random_uuid() PRIMARY KEY, + user_id UUID NOT NULL, + block_type VARCHAR(255) NOT NULL, + props JSON NOT NULL, + children TEXT[], + created_at TIMESTAMP DEFAULT NOW() NOT NULL, + updated_at TIMESTAMP DEFAULT NOW() NOT NULL +); + +SELECT diesel_manage_updated_at('blocks'); \ No newline at end of file diff --git a/backend/package.json b/backend/package.json deleted file mode 100644 index 21a2f92..0000000 --- a/backend/package.json +++ /dev/null @@ -1,36 +0,0 @@ -{ - "name": "todo", - "version": "1.0.0", - "description": "todo list app (because it hasnt been done before)", - "main": "src/index.js", - "scripts": { - "who": "pwd", - "start": "node src/index.js", - "test": "mocha" - }, - "repository": { - "type": "git", - "url": "git@ssh.gitdab.com:jane/todo.git" - }, - "author": "jane ", - "license": "SEE LICENSE IN LICENSE", - "dependencies": { - "cookie-parser": "^1.4.5", - "cors": "^2.8.5", - "express": "^4.17.1", - "express-paginate": "^1.0.2", - "http-proxy": "^1.18.1", - "node-fetch": "^2.6.1", - "nodemailer": "^6.6.2", - "pg": "^8.6.0", - "sequelize": "^6.6.5" - }, - "devDependencies": { - "chai": "^4.3.4", - "mocha": "^9.0.2", - "proxyrequire": "^1.0.21", - "sequelize-cli": "^6.2.0", - "sequelize-test-helpers": "^1.3.3", - "sinon": "^11.1.1" - } -} \ No newline at end of file diff --git a/backend/src/config.js b/backend/src/config.js deleted file mode 100644 index 47634ed..0000000 --- a/backend/src/config.js +++ /dev/null @@ -1,21 +0,0 @@ -const fs = require('fs'); - -if (!global.config) { - global.config = {} - const cfg = JSON.parse(fs.readFileSync('./config.json')); - if (cfg) { - global.config = cfg; - } -} - -class Config { - get config() { - return global.config; - } - - set config(dat) { - global.config = dat; - } -} - -module.exports = new Config(); \ No newline at end of file diff --git a/backend/src/db_interface.js b/backend/src/db_interface.js deleted file mode 100644 index 605c5cb..0000000 --- a/backend/src/db_interface.js +++ /dev/null @@ -1,20 +0,0 @@ -const Sequelize = require('sequelize'); -const Config = require('./config.js'); -const Models = require('./models'); - -if (!Config.config.db_url) { - console.error('No database url found. please set `db_url` in config.json'); - process.exit(); -} - -const db = new Sequelize(Config.config.db_url); - -module.exports = { - db: db, - constructors: { - user: () => { - return User.build(); - } - }, - schemas: Models(db, Sequelize) -}; diff --git a/backend/src/endpoints.rs b/backend/src/endpoints.rs new file mode 100644 index 0000000..284e2cf --- /dev/null +++ b/backend/src/endpoints.rs @@ -0,0 +1,30 @@ +use crate::diesel::PgConnection; +use crate::logging::LogService; +use async_redis_session::RedisSessionStore; +use axum::{prelude::*, routing::BoxRoute, AddExtensionLayer}; +use diesel::r2d2::{ConnectionManager, Pool}; +use std::env; + +pub mod block; +pub mod discord; +pub mod user; + +// this should never get called, because the reverse +// proxy on caddy should only direct calls from /api +async fn root() -> &'static str { + "Hi" +} + +pub fn get_routes(pool: Pool>) -> BoxRoute { + let redis_url = env::var("REDIS_URL").unwrap_or(String::from("redis://localhost")); + + let client = redis::Client::open(redis_url.as_str()).expect("Could not create redis client."); + route("/", get(root)) + .nest("/discord", discord::get_routes()) + .layer(tower::layer::layer_fn(|service| LogService { service })) + .layer(AddExtensionLayer::new(RedisSessionStore::from_client( + client, + ))) + .layer(AddExtensionLayer::new(pool)) + .boxed() +} diff --git a/backend/src/endpoints/block.rs b/backend/src/endpoints/block.rs new file mode 100644 index 0000000..e69de29 diff --git a/backend/src/endpoints/discord.rs b/backend/src/endpoints/discord.rs new file mode 100644 index 0000000..6d308d2 --- /dev/null +++ b/backend/src/endpoints/discord.rs @@ -0,0 +1,214 @@ +use async_redis_session::RedisSessionStore; +use async_session::{Session, SessionStore}; +use axum::{ + async_trait, + extract::{Extension, FromRequest, Query, RequestParts, TypedHeader}, + prelude::*, + response::IntoResponse, + routing::BoxRoute, + AddExtensionLayer, +}; +use http::{header::SET_COOKIE, StatusCode}; +use hyper::Body; +use oauth2::{ + basic::BasicClient, reqwest::async_http_client, AuthUrl, AuthorizationCode, ClientId, + ClientSecret, CsrfToken, RedirectUrl, Scope, TokenResponse, TokenUrl, +}; +use serde::{Deserialize, Serialize}; +use std::env; + +static COOKIE_NAME: &str = "SESSION"; + +fn oauth_client() -> BasicClient { + // Environment variables (* = required): + // *"CLIENT_ID" "123456789123456789"; + // *"CLIENT_SECRET" "rAn60Mch4ra-CTErsSf-r04utHcLienT"; + // "REDIRECT_URL" "http://127.0.0.1:3000/auth/authorized"; + // "AUTH_URL" "https://discord.com/api/oauth2/authorize?response_type=code"; + // "TOKEN_URL" "https://discord.com/api/oauth2/token"; + + let client_id = env::var("CLIENT_ID").expect("Missing CLIENT_ID!"); + let client_secret = env::var("CLIENT_SECRET").expect("Missing CLIENT_SECRET!"); + let redirect_url = env::var("REDIRECT_URL") + .unwrap_or_else(|_| "http://127.0.0.1:3000/auth/authorized".to_string()); + + let auth_url = env::var("AUTH_URL").unwrap_or_else(|_| { + "https://discord.com/api/oauth2/authorize?response_type=code".to_string() + }); + + let token_url = env::var("TOKEN_URL") + .unwrap_or_else(|_| "https://discord.com/api/oauth2/token".to_string()); + + BasicClient::new( + ClientId::new(client_id), + Some(ClientSecret::new(client_secret)), + AuthUrl::new(auth_url).unwrap(), + Some(TokenUrl::new(token_url).unwrap()), + ) + .set_redirect_uri(RedirectUrl::new(redirect_url).unwrap()) +} + +// The user data we'll get back from Discord. +// https://discord.com/developers/docs/resources/user#user-object-user-structure +#[derive(Debug, Serialize, Deserialize)] +struct DiscordUser { + id: String, + avatar: Option, + username: String, + discriminator: String, +} + +// Session is optional +async fn index(user: Option) -> impl IntoResponse { + match user { + Some(u) => format!( + "Hey {}! You're logged in!\nYou may now access `/protected`.\nLog out with `/logout`.", + u.username + ), + None => "You're not logged in.\nVisit `/auth/discord` to do so.".to_string(), + } +} + +async fn discord_auth(Extension(client): Extension) -> impl IntoResponse { + let (auth_url, _csrf_token) = client + .authorize_url(CsrfToken::new_random) + .add_scope(Scope::new("identify".to_string())) + .url(); + + // Redirect to Discord's oauth service + Redirect(auth_url.into()) +} + +// Valid user session required. If there is none, redirect to the auth page +async fn protected(user: DiscordUser) -> impl IntoResponse { + format!( + "Welcome to the protected area :)\nHere's your info:\n{:?}", + user + ) +} + +async fn logout( + Extension(store): Extension, + TypedHeader(cookies): TypedHeader, +) -> impl IntoResponse { + let cookie = cookies.get(COOKIE_NAME).unwrap(); + let session = match store.load_session(cookie.to_string()).await.unwrap() { + Some(s) => s, + // No session active, just redirect + None => return Redirect("/".to_string()), + }; + + store.destroy_session(session).await.unwrap(); + + Redirect("/".to_string()) +} + +#[derive(Debug, Deserialize)] +struct AuthRequest { + code: String, + state: String, +} + +async fn login_authorized( + Query(query): Query, + Extension(store): Extension, + Extension(oauth_client): Extension, +) -> impl IntoResponse { + // Get an auth token + let token = oauth_client + .exchange_code(AuthorizationCode::new(query.code.clone())) + .request_async(async_http_client) + .await + .unwrap(); + + // Fetch user data from discord + let client = reqwest::Client::new(); + let user_data: DiscordUser = client + // https://discord.com/developers/docs/resources/user#get-current-user + .get("https://discordapp.com/api/users/@me") + .bearer_auth(token.access_token().secret()) + .send() + .await + .unwrap() + .json::() + .await + .unwrap(); + + // Create a new session filled with user data + let mut session = Session::new(); + session.insert("user", &user_data).unwrap(); + + // Store session and get corresponding cookie + let cookie = store.store_session(session).await.unwrap().unwrap(); + + // Build the cookie + let cookie = format!("{}={}; SameSite=Lax; Path=/", COOKIE_NAME, cookie); + + // Set cookie + let r = http::Response::builder() + .header("Location", "/") + .header(SET_COOKIE, cookie) + .status(302); + + r.body(Body::empty()).unwrap() +} + +// Utility to save some lines of code +struct Redirect(String); +impl IntoResponse for Redirect { + fn into_response(self) -> http::Response { + let builder = http::Response::builder() + .header("Location", self.0) + .status(StatusCode::FOUND); + builder.body(Body::empty()).unwrap() + } +} + +struct AuthRedirect; +impl IntoResponse for AuthRedirect { + fn into_response(self) -> http::Response { + Redirect("/auth/discord".to_string()).into_response() + } +} + +#[async_trait] +impl FromRequest for DiscordUser +where + B: Send, +{ + // If anything goes wrong or no session is found, redirect to the auth page + type Rejection = AuthRedirect; + + async fn from_request(req: &mut RequestParts) -> Result { + let extract::Extension(store) = extract::Extension::::from_request(req) + .await + .expect("`RedisSessionStore` extension is missing"); + + let cookies: TypedHeader = + TypedHeader::::from_request(req) + .await + .expect("could not get cookies"); + + let session_cookie = cookies.0.get(COOKIE_NAME).ok_or(AuthRedirect)?; + + let session = store + .load_session(session_cookie.to_string()) + .await + .unwrap() + .ok_or(AuthRedirect)?; + + let user = session.get::("user").ok_or(AuthRedirect)?; + + Ok(user) + } +} + +pub fn get_routes() -> BoxRoute { + route("/", get(index)) + .route("/login/discord", get(discord_auth)) + .route("/authorized", get(login_authorized)) + .route("/protected", get(protected)) + .route("/logout", get(logout)) + .layer(AddExtensionLayer::new(oauth_client)) + .boxed() +} diff --git a/backend/src/endpoints/user.rs b/backend/src/endpoints/user.rs new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/backend/src/endpoints/user.rs @@ -0,0 +1 @@ + diff --git a/backend/src/helpers.rs b/backend/src/helpers.rs new file mode 100644 index 0000000..90708ad --- /dev/null +++ b/backend/src/helpers.rs @@ -0,0 +1,2 @@ +pub mod block; +pub mod user; diff --git a/backend/src/helpers/block.rs b/backend/src/helpers/block.rs new file mode 100644 index 0000000..00dd3b5 --- /dev/null +++ b/backend/src/helpers/block.rs @@ -0,0 +1,56 @@ +use crate::diesel::{prelude::*, PgConnection, QueryDsl}; +use crate::models::*; +use crate::schema::*; +use diesel::r2d2::{ConnectionManager, Pool}; +use std::error::Error; +use tokio_diesel::*; +use uuid::Uuid; + +pub async fn create_block( + pool: &Pool>, + block: InsertableBlock, +) -> Result> { + let inserted: Block = diesel::insert_into(blocks::table) + .values(block) + .get_result_async(pool) + .await?; + Ok(inserted) +} + +pub async fn update_block( + pool: &Pool>, + block: Block, +) -> Result> { + use crate::schema::blocks::dsl::*; + let result = diesel::update(blocks.filter(id.eq(block.id))) + .set(( + block_type.eq(block.block_type), + children.eq(block.children), + props.eq(block.props), + user_id.eq(block.user_id), + )) + .get_result_async(pool) + .await?; + Ok(result) +} + +pub async fn find_block_by_id( + pool: &Pool>, + block_id: Uuid, +) -> Result> { + use crate::schema::blocks::dsl::*; + let result: Block = blocks.find(block_id).get_result_async(pool).await?; + log::error!("{:?}", result); + Ok(result) +} + +pub async fn delete_block_by_id( + pool: &Pool>, + block_id: Uuid, +) -> Result> { + use crate::schema::blocks::dsl::*; + let result: Block = diesel::delete(blocks.filter(id.eq(block_id))) + .get_result_async(pool) + .await?; + Ok(result) +} diff --git a/backend/src/helpers/user.rs b/backend/src/helpers/user.rs new file mode 100644 index 0000000..abea039 --- /dev/null +++ b/backend/src/helpers/user.rs @@ -0,0 +1,51 @@ +use crate::diesel::{prelude::*, PgConnection, QueryDsl}; +use crate::models::*; +use crate::schema::*; +use diesel::r2d2::{ConnectionManager, Pool}; +use std::error::Error; +use tokio_diesel::*; +use uuid::Uuid; + +pub async fn create_user( + pool: &Pool>, + user: InsertableUser, +) -> Result> { + let inserted: User = diesel::insert_into(users::table) + .values(user) + .get_result_async(pool) + .await?; + Ok(inserted) +} + +pub async fn update_user( + pool: &Pool>, + user: User, +) -> Result> { + use crate::schema::users::dsl::*; + let result = diesel::update(users.filter(id.eq(user.id))) + .set((discord_id.eq(user.discord_id),)) + .get_result_async(pool) + .await?; + Ok(result) +} + +pub async fn find_user_by_id( + pool: &Pool>, + user_id: Uuid, +) -> Result> { + use crate::schema::users::dsl::*; + let result = users.find(user_id).get_result_async::(pool).await?; + log::error!("{:?}", result); + Ok(result) +} + +pub async fn delete_user_by_id( + pool: &Pool>, + user_id: Uuid, +) -> Result> { + use crate::schema::users::dsl::*; + let result = diesel::delete(users.filter(id.eq(user_id))) + .get_result_async(pool) + .await?; + Ok(result) +} diff --git a/backend/src/index.js b/backend/src/index.js deleted file mode 100644 index d3b4995..0000000 --- a/backend/src/index.js +++ /dev/null @@ -1,70 +0,0 @@ -const http = require('http'); -const https = require('https'); -const httpProxy = require('http-proxy'); -const cors = require('cors'); -const express = require('express'); -const cookieParser = require('cookie-parser'); -const fs = require('fs'); -const Config = require('./config.js'); - -const UserInterface = require('./user.js'); -const TodoInterface = require('./todo.js'); - -let credentials = {}; - -if (Config.config.https) { - if (fs.existsSync(Config.config.cert) && fs.existsSync(Config.config.cert_key)) { - credentials.key = fs.readFileSync(Config.config.cert_key); - credentials.cert = fs.readFileSync(Config.config.cert); - } - else { - console.error('could not load certs') - process.exit() - } -} - -let app = express(); - -app.use(cors()); -app.use(cookieParser()); - -// force https -app.use((req, res, next) => { - if (Config.config.https) { - if (req.headers['x-forwarded-proto'] !== 'https') { - return res.redirect(`https://${req.headers.host}${req.url}`); - } - } - return next(); -}); - -if (!Config.config.secret) { - console.error('No password secret found. please set `secret` in config.json'); - process.exit(); -} else if (Config.config.https && Config.config.secret == 'TEST_SECRET') { - console.error('please do not use the testing secret in production.'); - process.exit(); -} - -app.use('/api/user', UserInterface.router); -app.use('/api/todo', TodoInterface.router); - -if (Config.config.frontend_url) { - const proxy = httpProxy.createProxyServer({}) - app.use('/', (req, res) => { - return proxy.web(req, res, { - target: Config.config.frontend_url - }) - }); -} - -if (Config.config.https) { - var server = https.createServer(credentials, app); - server.listen(Config.config.port || 8080); -} else { - var server = http.createServer(app); - server.listen(Config.config.port || 8080); -} -console.log( - `listening on port ${Config.config.port || 8080}` + ` with https ${Config.config.https ? 'enabled' : 'disabled'}` -); diff --git a/backend/src/logging.rs b/backend/src/logging.rs new file mode 100644 index 0000000..6e0f587 --- /dev/null +++ b/backend/src/logging.rs @@ -0,0 +1,26 @@ +use std::fmt; +use std::task::{Context, Poll}; +use tower::Service; + +pub struct LogService { + pub service: S, +} + +impl Service for LogService +where + S: Service, + Request: fmt::Debug, +{ + type Response = S::Response; + type Error = S::Error; + type Future = S::Future; + + fn poll_ready(&mut self, cx: &mut Context<'_>) -> Poll> { + self.service.poll_ready(cx) + } + + fn call(&mut self, request: Request) -> Self::Future { + log::debug!("request = {:?}", request); + self.service.call(request) + } +} diff --git a/backend/src/mail.js b/backend/src/mail.js deleted file mode 100644 index 49e57c4..0000000 --- a/backend/src/mail.js +++ /dev/null @@ -1,52 +0,0 @@ -const Config = require('./config.js'); -const nodemailer = require('nodemailer'); - -class Mailer { - sender; - started = false; - mailer; - constructor(host, port, email, password) { - this.mailer = nodemailer.createTransport({ - host: host, - port: port, - secure: true, - auth: { - user: email, - pass: password - } - }); - this.sender = email; - this.started = true; - } - async sendMail(recipients, subject, content, contentStripped) { - console.log(`sending mail to ${recipients}`); - let info = await this.mailer.sendMail({ - from: `"Todo App" <${this.sender}>`, - to: Array.isArray(recipients) ? recipients.join(', ') : recipients, - subject: subject, - text: contentStripped, - html: content - }); - } -} - -if (!global.mailer || !global.mailer.started) { - if ( - !Config.config['mail_host'] || - !Config.config['mail_port'] || - !Config.config['mail_username'] || - !Config.config['mail_password'] - ) { - console.error(`could not create email account as -mail_host, mail_port, mail_username or mail_password is not set.`); - process.exit(); - } - global.mailer = new Mailer( - Config.config['mail_host'], - Config.config['mail_port'], - Config.config['mail_username'], - Config.config['mail_password'] - ); -} - -module.exports = global.mailer; diff --git a/backend/src/main.rs b/backend/src/main.rs new file mode 100644 index 0000000..b85d1fa --- /dev/null +++ b/backend/src/main.rs @@ -0,0 +1,66 @@ +use axum::prelude::*; +use std::net::SocketAddr; + +use diesel::{ + prelude::*, + r2d2::{ConnectionManager, Pool}, +}; +use dotenv::dotenv; +use std::env; +use std::str::FromStr; + +#[macro_use] +extern crate diesel; +extern crate redis; + +mod endpoints; +pub mod helpers; +pub mod logging; +pub mod migration; +pub mod models; +pub mod schema; +pub mod tests; + +#[tokio::main] +async fn main() { + dotenv().ok(); + let _ = setup_logger(); + + let db_url = env::var("DATABASE_URL").expect("DATABASE_URL is not set"); + + migration::run_migrations(&db_url); + let manager = ConnectionManager::::new(&db_url); + let pool = Pool::builder() + .build(manager) + .expect("Could not build connection pool"); + + let root = route("/api", endpoints::get_routes(pool)); + + let port = env::var("PORT").unwrap_or(String::from("8000")); + let addr = SocketAddr::from(([127, 0, 0, 1], port.parse().unwrap_or(8000))); + + log::info!("started listening on {:?}", addr); + hyper::Server::bind(&addr) + .serve(root.into_make_service()) + .await + .unwrap(); +} + +fn setup_logger() -> Result<(), fern::InitError> { + let log_level = env::var("LOG_LEVEL").unwrap_or(String::from("INFO")); + fern::Dispatch::new() + .format(|out, message, record| { + out.finish(format_args!( + "{} <{}> [{}] {}", + chrono::Local::now().format("%Y-%m-%d %H:%M:%S"), + record.file().unwrap_or(record.target()), + record.level(), + message + )) + }) + .level(log::LevelFilter::from_str(log_level.as_str()).unwrap_or(log::LevelFilter::Info)) + .chain(std::io::stdout()) + .chain(fern::log_file("latest.log")?) + .apply()?; + Ok(()) +} diff --git a/backend/src/migration.rs b/backend/src/migration.rs new file mode 100644 index 0000000..6b29fdd --- /dev/null +++ b/backend/src/migration.rs @@ -0,0 +1,22 @@ +use diesel::{prelude::*, sql_query}; +use diesel_migrations::*; + +pub fn reset_database(url: &String) { + let conn = PgConnection::establish(&url).expect(&format!("Error connecting to {}", url)); + println!("dropping all tables"); + let _ = sql_query("drop table users;").execute(&conn); + let _ = sql_query("drop table unverified_users;").execute(&conn); + let _ = sql_query("drop table blocks;").execute(&conn); + let _ = sql_query("drop table __diesel_schema_migrations;").execute(&conn); + println!("finished resetting db"); +} + +pub fn run_migrations(url: &String) { + println!("running migrations"); + let conn = PgConnection::establish(&url).expect(&format!("Error connecting to {}", url)); + let result = run_pending_migrations(&conn); + if result.is_err() { + panic!("could not run migrations: {}", result.err().unwrap()); + } + println!("finished migrations"); +} diff --git a/backend/src/models.js b/backend/src/models.js deleted file mode 100644 index 4b219e2..0000000 --- a/backend/src/models.js +++ /dev/null @@ -1,141 +0,0 @@ -const Sequelize = require('sequelize'); -const Config = require('./config.js'); - -const models = (db) => { - const UnverifiedUser = db.define('UnverifiedUser', { - id: { - type: Sequelize.DataTypes.UUID, - defaultValue: Sequelize.UUIDV4, - allowNull: false, - primaryKey: true, - unique: true - }, - verificationToken: { - type: Sequelize.DataTypes.STRING, - allowNull: false - }, - email: { - type: Sequelize.DataTypes.STRING, - allowNull: false, - unique: true - }, - discord_only_account: { - type: Sequelize.DataTypes.BOOLEAN, - allowNull: false, - defaultValue: false - }, - discord_id: { - type: Sequelize.DataTypes.STRING, - allowNull: true - }, - password_hash: { - type: Sequelize.DataTypes.STRING, - allowNull: true - } - }); - - const User = db.define('User', { - id: { - type: Sequelize.DataTypes.UUID, - defaultValue: Sequelize.UUIDV4, - allowNull: false, - primaryKey: true, - unique: true - }, - email: { - type: Sequelize.DataTypes.STRING, - allowNull: false, - unique: true - }, - discord_only_account: { - type: Sequelize.DataTypes.BOOLEAN, - allowNull: false, - defaultValue: false - }, - discord_id: { - type: Sequelize.DataTypes.STRING, - allowNull: true - }, - password_hash: { - type: Sequelize.DataTypes.STRING, - allowNull: true - } - }); - - const Todo = db.define('Todo', { - id: { - type: Sequelize.DataTypes.UUID, - defaultValue: Sequelize.UUIDV4, - allowNull: false, - primaryKey: true, - unique: true - }, - user: { - type: Sequelize.DataTypes.UUID, - allowNull: false - }, - content: { - type: Sequelize.DataTypes.TEXT, - allowNull: false - }, - tags: { - type: Sequelize.DataTypes.ARRAY(Sequelize.DataTypes.STRING), - allowNull: true - }, - complete: { - type: Sequelize.DataTypes.BOOLEAN, - allowNull: false, - defaultValue: false - }, - deadline: { - type: Sequelize.DataTypes.DATE, - allowNull: true - } - }); - - const Grouping = db.define('Grouping', { - id: { - type: Sequelize.DataTypes.UUID, - defaultValue: Sequelize.UUIDV4, - allowNull: false, - primaryKey: true, - unique: true - }, - complete: { - type: Sequelize.DataTypes.BOOLEAN, - allowNull: true - }, - manually_added: { - type: Sequelize.DataTypes.ARRAY(Sequelize.DataTypes.UUID), - allowNull: true - }, - required: { - type: Sequelize.DataTypes.ARRAY(Sequelize.DataTypes.STRING), - allowNull: true - }, - exclusions: { - type: Sequelize.DataTypes.ARRAY(Sequelize.DataTypes.STRING), - allowNull: true - } - }); - - let options = { - alter: false - }; - if (Config.config.alter_db) { - options.alter = true; - } - UnverifiedUser.sync(options); - User.sync(options); - Todo.sync(options); - Grouping.sync(options); - - return { - user: User, - unverifiedUser: UnverifiedUser, - todo: Todo, - grouping: Grouping - }; -}; - -module.exports = models; diff --git a/backend/src/models.rs b/backend/src/models.rs new file mode 100644 index 0000000..24a67c0 --- /dev/null +++ b/backend/src/models.rs @@ -0,0 +1,38 @@ +use super::schema::*; +use chrono::prelude::*; +use serde::{Deserialize, Serialize}; +use uuid::Uuid; + +#[derive(Queryable, Deserialize, Serialize, Debug, Clone, PartialEq)] +pub struct User { + pub id: Uuid, + pub discord_id: String, + pub created_at: NaiveDateTime, + pub updated_at: NaiveDateTime, +} + +#[derive(Insertable, Debug, Clone, PartialEq)] +#[table_name = "users"] +pub struct InsertableUser { + pub discord_id: String, +} + +#[derive(Queryable, Serialize, Deserialize, Debug, Clone, PartialEq)] +pub struct Block { + pub id: Uuid, + pub user_id: Uuid, + pub block_type: String, + pub props: serde_json::Value, + pub children: Option>, + pub created_at: NaiveDateTime, + pub updated_at: NaiveDateTime, +} + +#[derive(Insertable, Debug, Clone, PartialEq)] +#[table_name = "blocks"] +pub struct InsertableBlock { + pub user_id: Uuid, + pub block_type: String, + pub props: serde_json::Value, + pub children: Option>, +} diff --git a/backend/src/schema.rs b/backend/src/schema.rs new file mode 100644 index 0000000..3f30cdc --- /dev/null +++ b/backend/src/schema.rs @@ -0,0 +1,47 @@ +table! { + blocks (id) { + id -> Uuid, + user_id -> Uuid, + block_type -> Varchar, + props -> Json, + children -> Nullable>, + created_at -> Timestamp, + updated_at -> Timestamp, + } +} + +table! { + migration_versions (id) { + id -> Int4, + version -> Varchar, + } +} + +table! { + unverifiedusers (id) { + id -> Varchar, + email -> Nullable, + discord_only_account -> Nullable, + discord_id -> Nullable, + password_hash -> Nullable, + verification_token -> Nullable, + created_at -> Timestamp, + updated_at -> Timestamp, + } +} + +table! { + users (id) { + id -> Uuid, + discord_id -> Varchar, + created_at -> Timestamp, + updated_at -> Timestamp, + } +} + +allow_tables_to_appear_in_same_query!( + blocks, + migration_versions, + unverifiedusers, + users, +); diff --git a/backend/src/tests.rs b/backend/src/tests.rs new file mode 100644 index 0000000..e9e4d7e --- /dev/null +++ b/backend/src/tests.rs @@ -0,0 +1,18 @@ +pub mod db; + +#[cfg(test)] +mod tests { + // a basic test to ensure that tests are executing. + #[test] + fn works() { + assert_eq!(1, 1); + } + + #[tokio::test(flavor = "multi_thread")] + async fn db_tests() { + let url = String::from("postgres://postgres@localhost/todo_test"); + crate::migration::reset_database(&url); + crate::migration::run_migrations(&url); + super::db::run_tests(&url).await; + } +} diff --git a/backend/src/tests/db.rs b/backend/src/tests/db.rs new file mode 100644 index 0000000..ff3f569 --- /dev/null +++ b/backend/src/tests/db.rs @@ -0,0 +1,132 @@ +use crate::helpers::*; +use crate::models::*; +use diesel::{ + r2d2::{ConnectionManager, Pool}, + PgConnection, +}; +use std::{error::Error, vec::Vec}; +use uuid::Uuid; + +async fn get_pool(url: &String) -> Pool> { + let manager = ConnectionManager::::new(url); + Pool::builder() + .build(manager) + .expect("Could not build connection pool") +} + +async fn user_tests(pool: &Pool>) { + let user = InsertableUser { + discord_id: String::from("test"), + }; + let created_result = user::create_user(pool, user).await; + if created_result.is_err() { + panic!("could not create user: {:?}", created_result.err()); + } else { + assert!(created_result.is_ok()); + } + + let created_user: User = created_result.unwrap(); + + let mut new_user = created_user.clone(); + new_user.discord_id = String::from("test2"); + let user_update = new_user.clone(); + + let updated_result: Result> = user::update_user(pool, user_update).await; + + if updated_result.is_err() { + panic!( + "cound not update user {}: {:?}", + created_user.id, + updated_result.err() + ); + } + + let updated_user = updated_result.unwrap(); + assert_eq!(new_user.id, updated_user.id); + assert_eq!(new_user.discord_id, updated_user.discord_id); + + let get_result = user::find_user_by_id(pool, created_user.id).await; + if get_result.is_err() { + panic!( + "could not find previously created user {}: {:?}", + created_user.id, + get_result.err() + ); + } else { + assert_eq!(updated_user, get_result.unwrap()); + } + + let delete_result = user::delete_user_by_id(pool, created_user.id).await; + if delete_result.is_err() { + panic!( + "could not delete user {}: {:?}", + created_user.id, + delete_result.err() + ); + } +} + +async fn block_tests(pool: &Pool>) { + let json = serde_json::from_str("[]"); + let block = InsertableBlock { + user_id: Uuid::new_v4(), + block_type: String::from("test"), + children: Some(Vec::new()), + props: json.unwrap(), + }; + let created_result = block::create_block(pool, block).await; + if created_result.is_err() { + panic!("could not create block: {:?}", created_result.err()); + } else { + assert!(created_result.is_ok()); + } + + let created_block: Block = created_result.unwrap(); + + let mut new_block = created_block.clone(); + new_block.block_type = String::from("test2"); + let block_update = new_block.clone(); + + let updated_result: Result> = + block::update_block(pool, block_update).await; + + if updated_result.is_err() { + panic!( + "cound not update block {}: {:?}", + created_block.id, + updated_result.err() + ); + } + let updated_block = updated_result.unwrap(); + assert_eq!(new_block.id, updated_block.id); + assert_eq!(new_block.block_type, updated_block.block_type); + assert_eq!(new_block.user_id, updated_block.user_id); + assert_eq!(new_block.children, updated_block.children); + assert_eq!(new_block.props, updated_block.props); + + let get_result = block::find_block_by_id(pool, created_block.id).await; + if get_result.is_err() { + panic!( + "could not find previously created block {}: {:?}", + created_block.id, + get_result.err() + ); + } else { + assert_eq!(updated_block, get_result.unwrap()); + } + + let delete_result = block::delete_block_by_id(pool, created_block.id).await; + if delete_result.is_err() { + panic!( + "could not delete block {}: {:?}", + created_block.id, + delete_result.err() + ); + } +} + +pub async fn run_tests(url: &String) { + let pool = get_pool(url).await; + user_tests(&pool).await; + block_tests(&pool).await; +} diff --git a/backend/src/todo.js b/backend/src/todo.js deleted file mode 100644 index bc31195..0000000 --- a/backend/src/todo.js +++ /dev/null @@ -1,146 +0,0 @@ -const express = require('express'); -const paginate = require('express-paginate'); -const Database = require('./db_interface.js'); -const User = require('./user.js'); -const { Op } = require('sequelize'); - -let router = express.Router(); - -router.use(express.json()); - -function map_todo(result) { - return { - id: result.id, - content: result.content, - tags: result.tags - }; -} - -function parse_tags(tags) { - result = { - complete: undefined, - required: [], - excluded: [] - }; - tags.map((tag) => { - if (tag === 'complete') { - complete = true; - } else if (tag === '~complete') { - complete = false; - } else if (tag.startsWith('~')) { - excluded.push(tag); - } else { - required.push(tag); - } - }); - return result; -} - -const todo_fields = ['currentPage', 'limit']; - -router.use(paginate.middleware(10, 50)); -router.use('/todos', User.enforce_session_login); -router.get('/todos', async (req, res) => { - if (!req.query) { - return res.status(400).json({ - error: `query must include the fields: ${todo_fields.join(', ')}}` - }); - } else { - let error = []; - for (let field of todo_fields) { - if (!req.query[field]) { - error.push(field); - } - } - if (error.length > 0) { - return res.status(400).json({ - error: `query must include the fields: ${error.join(', ')}}` - }); - } - } - let tag_options = {}; - if (req.query.tags) { - let parsed = parse_tags(req.query.tags.split(',')); - tag_options['tags'] = { - [Op.and]: parsed.required, - [Op.not]: parsed.excluded - }; - if (parsed.complete !== undefined) { - tag_options['complete'] = { - [Op.is]: parsed.complete - }; - } - } - console.log(tag_options); - let all_todos = await Database.schemas.todo.findAndCountAll({ - where: { - user: req.get('id'), - ...tag_options - }, - limit: req.query.limit, - offset: req.skip - }); - const item_count = all_todos.count; - const page_count = Math.ceil(item_count / req.query.limit); - res.json({ - result: all_todos.map(map_todo), - currentPage: req.query.currentPage, - pageCount: page_count, - itemCount: item_count, - pages: paginate.getArrayPages(req)(5, page_count, req.query.currentPage) - }); -}); - -router.use('/todo', User.enforce_session_login); -router.get('/todo/:id([a-f0-9-]+)', async (req, res) => { - let userid = req.get('id'); - let id = req.params?.id; - - let match = await Database.schemas.todo.findOne({ - where: { - user: userid, - id: id - } - }); - if (!match) { - return res.sendStatus(404); - } - - return res.json({ - result: map_todo(match), - tags: get_tags(match.id) - }); -}); - -router.use('/todo', User.enforce_session_login); -router.post('/todo/:id([a-f0-9-]+)', async (req, res) => { - let userid = req.get('id'); - let id = req.params?.id; - - let body = req.body; - - if (!body) { - return res.sendStatus(400); - } - - let match = await Database.schemas.todo.findOne({ - where: { - user: userid, - id: id - } - }); - if (!match) { - return res.sendStatus(404); - } - - // - - return res.json({ - result: map_todo(match), - tags: get_tags(match.id) - }); -}); - -module.exports = { - router: router -}; diff --git a/backend/src/user.js b/backend/src/user.js deleted file mode 100644 index 151d1c3..0000000 --- a/backend/src/user.js +++ /dev/null @@ -1,424 +0,0 @@ -const express = require('express'); -const crypto = require('crypto'); -const fetch = require('node-fetch'); -const Config = require('./config.js'); -const Database = require('./db_interface.js'); -const Mail = require('./mail.js'); - -let router = express.Router(); - -router.use(express.json()); - -let session_entropy = {}; - -user_cache = {}; -email_cache = {}; -discord_cache = {}; -discord_user_cache = {}; - -async function fetch_user(where) { - let user = await Database.schemas.user.findOne({ where: where }); - if (user === null) { - return undefined; - } - user_cache[user.id] = { - id: user.id, - email: user.email, - discord_id: user.discord_id, - password_hash: user.password_hash - }; - email_cache[user.email] = user.id; - if (user.discord_id) { - discord_cache[user.discord_id] = user.id - } - return user_cache[user.id] -} - -async function fetch_discord_user(auth) { - const result = await fetch(`https://discord.com/api/v8/users/@me`, { - headers: { - 'Authorization': auth.token_type + ' ' + auth.access_token - } - }); - const json = result.json(); - discord_user_cache[json.id] = { - user: json, - auth: auth, - expires: (new Date().getTime()) + (json.expires_in * 1000) - } - return discord_user_cache[id]; -} - -async function acquire_discord_token(code, redirect) { - let data = { - client_id: Config.config.discord_id, - client_secret: Config.config.discord_secret, - grant_type: 'authorization_code', - code: code, - redirect_uri: redirect - } - const result = await fetch(`https://discord.com/api/oauth2/token`, { - method: 'POST', - body: data, - headers: { - 'Content-Type': 'application/x-www-form-urlencoded' - } - }).catch(err => console.error(err)); - if (!result.ok) { - return res.status(500).json({error: "could not fetch user details"}) - } - const json = result.json(); - return fetch_discord_user(json); -} - -async function refresh_discord_token(id) { - let data = { - client_id: Config.config.discord_id, - client_secret: Config.config.discord_secret, - grant_type: 'refresh_token', - refresh_token: discord_user_cache[id].auth.refresh_token - } - const result = await fetch(`https://discord.com/api/oauth2/token`, { - method: 'POST', - body: data, - headers: { - 'Content-Type': 'application/x-www-form-urlencoded' - } - }).catch(err => console.error(err)); - if (!result.ok) { - return false; - } - const json = result.json(); - discord_user_cache[id].auth.access_token = json.access_token; - discord_user_cache[id].expires = (new Date().getTime()) + (json.expires_in * 1000); - return true; -} - -async function get_user_details(id) { - if (!id || id === 'undefined') { - return undefined; - } - console.log(`search for user with id ${id}`); - if (!user_cache[id]) { - return await fetch_user({ id: id }) - } - // console.log(`returning ${JSON.stringify(user_cache[id])}`); - return user_cache[id]; -} - -async function get_user_details_by_email(email) { - if (!email || email === 'undefined') { - return undefined; - } - console.log(`search for user with email ${email}}`); - if (!email_cache[email] || !user_cache[email_cache[email]]) { - return await fetch_user({ email: email }) - } - // console.log(`returning ${JSON.stringify(user_cache[email_cache[email]])}`); - return user_cache[email_cache[email]]; -} - -async function get_user_details_by_discord_id(id) { - if (!id || id === 'undefined') { - return undefined; - } - if (!discord_cache[id] || !user_cache[discord_cache[id]]) { - return await fetch_user({ discord_id: id }) - } - return user_cache[discord_cache[id]]; -} - -function hash(secret, password, base64 = true) { - let pw_hash = crypto.pbkdf2Sync( - password, - secret, - Config.config.key?.iterations || 1000, - Config.config.key?.length || 64, - 'sha512' - ); - - return pw_hash.toString(base64 ? 'base64' : 'hex'); -} - -function verify(secret, password, hash) { - let pw_hash = crypto.pbkdf2Sync( - password, - secret, - Config.config.key?.iterations || 1000, - Config.config.key?.length || 64, - 'sha512' - ); - - return hash === pw_hash.toString('base64'); -} - -function hash_password(password) { - return hash(Config.config.secret, password); -} - -function verify_password(password, hash) { - return verify(Config.config.secret, password, hash); -} - -function get_session_token(id, password_hash, base64 = true) { - session_entropy[id] = crypto.randomBytes(Config.config.session_entropy || 32); - return hash(session_entropy[id], password_hash, base64); -} - -function verify_session_token(id, hash, token) { - if (session_entropy[id]) { - return verify(session_entropy[id], hash, token); - } else { - return false; - } -} - -async function enforce_session_login(req, res, next) { - let userid = req.get('id'); - let session_token = req.get('authorization'); - console.log('a', userid, session_token); - if (!userid || !session_token) { - return res.sendStatus(401); - } - let user = await get_user_details(userid); - if (!user) { - return res.sendStatus(401); - } - let verified_session = verify_session_token(userid, user.password_hash, session_token); - if (!verified_session) { - return res.sendStatus(401); - } - return next(); -} - -router.post('/signup', async (req, res) => { - if (!req.body?.email || !req.body?.password) { - return res.status(400).json({ - error: 'must have email and password fields' - }); - } - let user = await get_user_details_by_email(req.body?.email); - - if (user !== undefined && user !== {}) { - console.warn(`user already found: ${JSON.stringify(user)}`); - return res.status(403).json({ - error: `email ${req.body.email} is already in use.` - }); - } else { - let match = await Database.schemas.unverifiedUser.findOne({ where: { email: req.body.email } }); - if (!!match) { - await Database.schemas.unverifiedUser.destroy({ where: { email: match.email } }); - } - let randomString = 'Signup'; - for (let i = 0; i < 16; i++) { - randomString += Math.floor(Math.random() * 10); - } - let password_hash = hash_password(req.body.password); - let user = await Database.schemas.unverifiedUser.create({ - email: String(req.body.email), - password_hash: password_hash, - verificationToken: get_session_token(randomString, password_hash, false) - }); - const link = `${Config.config.https ? 'https://' : 'http://'}${req.headers.host}/api/user/verify?verification=${user.verificationToken - }`; - const content = `Click here to verify your sign-up: -${link}`; - const contentHtml = `

Click here to verify your sign-up:

-

${link}

`; - await Mail.sendMail([String(req.body.email)], 'Verify Your Account', contentHtml, content); - return res.sendStatus(204); - } -}); - -router.get('/verify', async (req, res) => { - if (!req.query?.verification) { - return res.status(400).send( - ` - -

No Verification Link

- - ` - ); - } - let verification = req.query?.verification; - let user = await Database.schemas.unverifiedUser.findOne({ where: { verificationToken: verification } }); - - if (user !== undefined && user !== {}) { - if (user.verificationToken != verification) { - return res.status(404).send( - ` - -

Unknown Verification Link

- - ` - ); - } - let newUser = await Database.schemas.user.create({ - email: user.email, - password_hash: user.password_hash - }); - - return res.send(` - -

Sign up complete.

- - `); - } else { - return res.status(404).send(` - -

Unknown Verification Link

- - `); - } -}); - -router.get('/login/discord', async (req, res) => { - if (!Config.config.discord_id || !Config.config.discord_secret) { - return res.status(403).send("discord login is not enabled."); - } - const url = encodeURIComponent(`${req.headers.host}discord`); - return res.send(`https://discord.com/api/oauth2/authorize?client_id=${Config.config.discord_id}&redirect_uri=${url}&response_type=code&scope=identify%20email%20guilds`); -}); - -router.post('/login/discord', async (req, res) => { - if (!Config.config.discord_id || !Config.config.discord_secret) { - return res.status(403).json({ error: "discord login is not enabled." }); - } - if (!req.params.code || !req.headers.host) { - return res.status(400).json({error: "invalid oauth request"}); - } - const result = await acquire_discord_token(req.params.code, req.headers.host); - const matching_account = await get_user_details_by_discord_id(result.user.id); - if (!matching_account) { - let user = await Database.schemas.unverifiedUser.create({ - email: String(result.user.email), - discord_id: user.id, - verificationToken: get_session_token(randomString, result.auth.access_token, false) - }); - return res.json({ - type: 'unverified', - verificationToken: user.verificationToken - }) - } - return res.json({ - type: 'verified', - userid: matching_account.id, - session_token: get_session_token(matching_account.id, result.auth.access_token) - }); -}); - -//TODO -router.post('/discord/create', async (req, res) =>{}); -router.post('/discord/link', async (req, res) =>{}); - -router.post('/login', async (req, res) => { - if (!req.body?.email || !req.body?.password) { - return res.status(400).json({ - error: 'must have email and password fields' - }); - } - let user = await get_user_details_by_email(req.body.email); - if (!user) { - return res.status(401).json({ - error: 'incorrect email or password' - }); - } - let verified = verify_password(req.body.password, user.password_hash); - - if (!verified) { - return res.status(401).json({ - error: 'incorrect email or password' - }); - } - - return res.json({ - userid: user.id, - session_token: get_session_token(user.id, user.password_hash) - }); -}); - - -router.use('/logout', enforce_session_login) -router.post('/logout', async (req, res) => { - let userid = req.get('id'); - let session_token = req.get('authorization'); - - let user = await get_user_details(userid); - if (!user) { - return res.status(401).json({ - error: 'invalid user data' - }); - } - let verified = verify_session_token(user.id, user.password_hash, session_token); - - if (!verified) { - return res.status(401).json({ - error: 'invalid user data' - }); - } - - delete session_entropy[user.id]; - return res.sendStatus(204); -}); - -router.use('/byEmail', enforce_session_login) -router.get('/byEmail/:email', async (req, res) => { - if (!req.params?.email) { - res.status(400).json({ - error: 'email is a required parameter' - }); - } - let user = get_user_details_by_email(req.params.email); - console.log(user); - if (user !== undefined && user !== {}) { - res.json({ - id: user.id, - email: user.email - }); - } else { - res.sendStatus(404); - } -}); - - -router.use('/', enforce_session_login) -router.get('/:id([a-f0-9-]+)', async (req, res) => { - console.log(req.params); - if (!req.params?.id) { - return res.status(400).json({ - error: 'must have id parameter' - }); - } - let id = req.params?.id; - console.log(id); - let user = await get_user_details(id); - console.log(user); - if (user !== undefined && user !== {}) { - return res.json({ - id: user.id, - email: user.email - }); - } else { - return res.sendStatus(404); - } -}); - -router.use('/authorized', enforce_session_login); -router.get('/authorized', async (req, res) => { - let userid = req.get('id'); - let user = await get_user_details(userid); - return res.json({ - authorized: true, - user: { - id: user.id, - email: user.email - } - }); -}); - -module.exports = { - router: router, - enforce_session_login: enforce_session_login, - get_user_details: get_user_details, - get_user_details_by_email: get_user_details_by_email -}; diff --git a/backend/test/db_interface.spec.js b/backend/test/db_interface.spec.js deleted file mode 100644 index cf2fe82..0000000 --- a/backend/test/db_interface.spec.js +++ /dev/null @@ -1,17 +0,0 @@ -const { expect } = require('chai'); -const Models = require('../src/models'); - -const { sequelize, checkModelName, checkUniqueIndex, checkPropertyExists } = require('sequelize-test-helpers'); - -describe('Sequelize model tests', function () { - const models = Models(sequelize); - - checkModelName(models.user)('User'); - checkModelName(models.unverifiedUser)('UnverifiedUser'); - checkModelName(models.grouping)('Grouping'); - checkModelName(models.todo)('Todo'); - - context('user props', function () { - ['id', 'email', 'discord_only_account'].forEach(checkPropertyExists(new models.user())); - }); -}); diff --git a/backend/test/user.spec.js b/backend/test/user.spec.js deleted file mode 100644 index 2861017..0000000 --- a/backend/test/user.spec.js +++ /dev/null @@ -1,10 +0,0 @@ -const { expect } = require('chai'); -const proxyrequire = require('proxyrequire'); -const { match, stub, resetHistory } = require('sinon'); -const { sequelize, Sequelize, makeMockModels } = require('sequelize-test-helpers'); - -describe('User Router Tests', function () { - const Database = proxyrequire('../src/db_interface', { - sequelize: Sequelize - }); -}); diff --git a/frontend/.gitignore b/frontend/.gitignore deleted file mode 100644 index 4d29575..0000000 --- a/frontend/.gitignore +++ /dev/null @@ -1,23 +0,0 @@ -# See https://help.github.com/articles/ignoring-files/ for more about ignoring files. - -# dependencies -/node_modules -/.pnp -.pnp.js - -# testing -/coverage - -# production -/build - -# misc -.DS_Store -.env.local -.env.development.local -.env.test.local -.env.production.local - -npm-debug.log* -yarn-debug.log* -yarn-error.log* diff --git a/frontend/README.md b/frontend/README.md index 02aac3f..7b1ba83 100644 --- a/frontend/README.md +++ b/frontend/README.md @@ -1,70 +1,105 @@ -# Getting Started with Create React App +*Looking for a shareable component template? Go here --> [sveltejs/component-template](https://github.com/sveltejs/component-template)* -This project was bootstrapped with [Create React App](https://github.com/facebook/create-react-app). +--- -## Available Scripts +# svelte app -In the project directory, you can run: +This is a project template for [Svelte](https://svelte.dev) apps. It lives at https://github.com/sveltejs/template. -### `yarn start` +To create a new project based on this template using [degit](https://github.com/Rich-Harris/degit): -Runs the app in the development mode.\ -Open [http://localhost:3000](http://localhost:3000) to view it in the browser. +```bash +npx degit sveltejs/template svelte-app +cd svelte-app +``` -The page will reload if you make edits.\ -You will also see any lint errors in the console. +*Note that you will need to have [Node.js](https://nodejs.org) installed.* -### `yarn test` -Launches the test runner in the interactive watch mode.\ -See the section about [running tests](https://facebook.github.io/create-react-app/docs/running-tests) for more information. +## Get started -### `yarn build` +Install the dependencies... -Builds the app for production to the `build` folder.\ -It correctly bundles React in production mode and optimizes the build for the best performance. +```bash +cd svelte-app +npm install +``` -The build is minified and the filenames include the hashes.\ -Your app is ready to be deployed! +...then start [Rollup](https://rollupjs.org): -See the section about [deployment](https://facebook.github.io/create-react-app/docs/deployment) for more information. +```bash +npm run dev +``` -### `yarn eject` +Navigate to [localhost:5000](http://localhost:5000). You should see your app running. Edit a component file in `src`, save it, and reload the page to see your changes. -**Note: this is a one-way operation. Once you `eject`, you can’t go back!** +By default, the server will only respond to requests from localhost. To allow connections from other computers, edit the `sirv` commands in package.json to include the option `--host 0.0.0.0`. -If you aren’t satisfied with the build tool and configuration choices, you can `eject` at any time. This command will remove the single build dependency from your project. +If you're using [Visual Studio Code](https://code.visualstudio.com/) we recommend installing the official extension [Svelte for VS Code](https://marketplace.visualstudio.com/items?itemName=svelte.svelte-vscode). If you are using other editors you may need to install a plugin in order to get syntax highlighting and intellisense. -Instead, it will copy all the configuration files and the transitive dependencies (webpack, Babel, ESLint, etc) right into your project so you have full control over them. All of the commands except `eject` will still work, but they will point to the copied scripts so you can tweak them. At this point you’re on your own. +## Building and running in production mode -You don’t have to ever use `eject`. The curated feature set is suitable for small and middle deployments, and you shouldn’t feel obligated to use this feature. However we understand that this tool wouldn’t be useful if you couldn’t customize it when you are ready for it. +To create an optimised version of the app: -## Learn More +```bash +npm run build +``` -You can learn more in the [Create React App documentation](https://facebook.github.io/create-react-app/docs/getting-started). +You can run the newly built app with `npm run start`. This uses [sirv](https://github.com/lukeed/sirv), which is included in your package.json's `dependencies` so that the app will work when you deploy to platforms like [Heroku](https://heroku.com). -To learn React, check out the [React documentation](https://reactjs.org/). -### Code Splitting +## Single-page app mode -This section has moved here: [https://facebook.github.io/create-react-app/docs/code-splitting](https://facebook.github.io/create-react-app/docs/code-splitting) +By default, sirv will only respond to requests that match files in `public`. This is to maximise compatibility with static fileservers, allowing you to deploy your app anywhere. -### Analyzing the Bundle Size +If you're building a single-page app (SPA) with multiple routes, sirv needs to be able to respond to requests for *any* path. You can make it so by editing the `"start"` command in package.json: -This section has moved here: [https://facebook.github.io/create-react-app/docs/analyzing-the-bundle-size](https://facebook.github.io/create-react-app/docs/analyzing-the-bundle-size) +```js +"start": "sirv public --single" +``` -### Making a Progressive Web App +## Using TypeScript -This section has moved here: [https://facebook.github.io/create-react-app/docs/making-a-progressive-web-app](https://facebook.github.io/create-react-app/docs/making-a-progressive-web-app) +This template comes with a script to set up a TypeScript development environment, you can run it immediately after cloning the template with: -### Advanced Configuration +```bash +node scripts/setupTypeScript.js +``` -This section has moved here: [https://facebook.github.io/create-react-app/docs/advanced-configuration](https://facebook.github.io/create-react-app/docs/advanced-configuration) +Or remove the script via: -### Deployment +```bash +rm scripts/setupTypeScript.js +``` -This section has moved here: [https://facebook.github.io/create-react-app/docs/deployment](https://facebook.github.io/create-react-app/docs/deployment) +## Deploying to the web -### `yarn build` fails to minify +### With [Vercel](https://vercel.com) -This section has moved here: [https://facebook.github.io/create-react-app/docs/troubleshooting#npm-run-build-fails-to-minify](https://facebook.github.io/create-react-app/docs/troubleshooting#npm-run-build-fails-to-minify) +Install `vercel` if you haven't already: + +```bash +npm install -g vercel +``` + +Then, from within your project folder: + +```bash +cd public +vercel deploy --name my-project +``` + +### With [surge](https://surge.sh/) + +Install `surge` if you haven't already: + +```bash +npm install -g surge +``` + +Then, from within your project folder: + +```bash +npm run build +surge public my-project.surge.sh +``` diff --git a/frontend/package.json b/frontend/package.json index e06b6a8..7c2d125 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -1,50 +1,23 @@ { - "name": "frontend", - "version": "0.1.0", + "name": "svelte-app", + "version": "1.0.0", "private": true, - "dependencies": { - "@emotion/react": "^11.4.0", - "@emotion/styled": "^11.3.0", - "@material-ui/core": "^5.0.0-beta.0", - "@material-ui/icons": "^4.11.2", - "@material-ui/styles": "^4.11.4", - "@reduxjs/toolkit": "^1.6.0", - "@testing-library/jest-dom": "^5.14.1", - "@testing-library/react": "^11.2.7", - "@testing-library/user-event": "^12.8.3", - "axios": "^0.21.1", - "react": "^17.0.2", - "react-dom": "^17.0.2", - "react-redux": "^7.2.4", - "react-router": "^5.2.0", - "react-router-dom": "^5.2.0", - "react-scripts": "4.0.3", - "redux-logger": "^3.0.6", - "redux-thunk": "^2.3.0", - "web-vitals": "^1.1.2" - }, "scripts": { - "start": "react-scripts start", - "build": "react-scripts build", - "test": "react-scripts test", - "eject": "react-scripts eject" + "build": "rollup -c", + "dev": "rollup -c -w", + "start": "sirv public --no-clear --host --port 3000" }, - "eslintConfig": { - "extends": [ - "react-app", - "react-app/jest" - ] + "devDependencies": { + "@rollup/plugin-commonjs": "^17.0.0", + "@rollup/plugin-node-resolve": "^11.0.0", + "rollup": "^2.3.4", + "rollup-plugin-css-only": "^3.1.0", + "rollup-plugin-livereload": "^2.0.0", + "rollup-plugin-svelte": "^7.0.0", + "rollup-plugin-terser": "^7.0.0", + "svelte": "^3.0.0" }, - "browserslist": { - "production": [ - ">0.2%", - "not dead", - "not op_mini all" - ], - "development": [ - "last 1 chrome version", - "last 1 firefox version", - "last 1 safari version" - ] + "dependencies": { + "sirv-cli": "^1.0.0" } } diff --git a/frontend/public/build/bundle.css b/frontend/public/build/bundle.css new file mode 100644 index 0000000..e6777aa --- /dev/null +++ b/frontend/public/build/bundle.css @@ -0,0 +1 @@ +.svelte-lxvsk7{font-family:inherit;font-size:inherit}input.svelte-lxvsk7{display:block;margin:0 0 0.5em 0}select.svelte-lxvsk7{float:left;margin:0 1em 1em 0;width:14em}.buttons.svelte-lxvsk7{clear:both} \ No newline at end of file diff --git a/frontend/public/build/bundle.js b/frontend/public/build/bundle.js new file mode 100644 index 0000000..cc07edd --- /dev/null +++ b/frontend/public/build/bundle.js @@ -0,0 +1,801 @@ + +(function(l, r) { if (!l || l.getElementById('livereloadscript')) return; r = l.createElement('script'); r.async = 1; r.src = '//' + (self.location.host || 'localhost').split(':')[0] + ':35729/livereload.js?snipver=1'; r.id = 'livereloadscript'; l.getElementsByTagName('head')[0].appendChild(r) })(self.document); +var app = (function () { + 'use strict'; + + function noop() { } + function add_location(element, file, line, column, char) { + element.__svelte_meta = { + loc: { file, line, column, char } + }; + } + function run(fn) { + return fn(); + } + function blank_object() { + return Object.create(null); + } + function run_all(fns) { + fns.forEach(run); + } + function is_function(thing) { + return typeof thing === 'function'; + } + function safe_not_equal(a, b) { + return a != a ? b == b : a !== b || ((a && typeof a === 'object') || typeof a === 'function'); + } + function is_empty(obj) { + return Object.keys(obj).length === 0; + } + function append(target, node) { + target.appendChild(node); + } + function insert(target, node, anchor) { + target.insertBefore(node, anchor || null); + } + function detach(node) { + node.parentNode.removeChild(node); + } + function destroy_each(iterations, detaching) { + for (let i = 0; i < iterations.length; i += 1) { + if (iterations[i]) + iterations[i].d(detaching); + } + } + function element(name) { + return document.createElement(name); + } + function text(data) { + return document.createTextNode(data); + } + function space() { + return text(' '); + } + function listen(node, event, handler, options) { + node.addEventListener(event, handler, options); + return () => node.removeEventListener(event, handler, options); + } + function attr(node, attribute, value) { + if (value == null) + node.removeAttribute(attribute); + else if (node.getAttribute(attribute) !== value) + node.setAttribute(attribute, value); + } + function children(element) { + return Array.from(element.childNodes); + } + function set_input_value(input, value) { + input.value = value == null ? '' : value; + } + function select_option(select, value) { + for (let i = 0; i < select.options.length; i += 1) { + const option = select.options[i]; + if (option.__value === value) { + option.selected = true; + return; + } + } + } + function select_value(select) { + const selected_option = select.querySelector(':checked') || select.options[0]; + return selected_option && selected_option.__value; + } + function custom_event(type, detail, bubbles = false) { + const e = document.createEvent('CustomEvent'); + e.initCustomEvent(type, bubbles, false, detail); + return e; + } + + let current_component; + function set_current_component(component) { + current_component = component; + } + + const dirty_components = []; + const binding_callbacks = []; + const render_callbacks = []; + const flush_callbacks = []; + const resolved_promise = Promise.resolve(); + let update_scheduled = false; + function schedule_update() { + if (!update_scheduled) { + update_scheduled = true; + resolved_promise.then(flush); + } + } + function add_render_callback(fn) { + render_callbacks.push(fn); + } + let flushing = false; + const seen_callbacks = new Set(); + function flush() { + if (flushing) + return; + flushing = true; + do { + // first, call beforeUpdate functions + // and update components + for (let i = 0; i < dirty_components.length; i += 1) { + const component = dirty_components[i]; + set_current_component(component); + update(component.$$); + } + set_current_component(null); + dirty_components.length = 0; + while (binding_callbacks.length) + binding_callbacks.pop()(); + // then, once components are updated, call + // afterUpdate functions. This may cause + // subsequent updates... + for (let i = 0; i < render_callbacks.length; i += 1) { + const callback = render_callbacks[i]; + if (!seen_callbacks.has(callback)) { + // ...so guard against infinite loops + seen_callbacks.add(callback); + callback(); + } + } + render_callbacks.length = 0; + } while (dirty_components.length); + while (flush_callbacks.length) { + flush_callbacks.pop()(); + } + update_scheduled = false; + flushing = false; + seen_callbacks.clear(); + } + function update($$) { + if ($$.fragment !== null) { + $$.update(); + run_all($$.before_update); + const dirty = $$.dirty; + $$.dirty = [-1]; + $$.fragment && $$.fragment.p($$.ctx, dirty); + $$.after_update.forEach(add_render_callback); + } + } + const outroing = new Set(); + function transition_in(block, local) { + if (block && block.i) { + outroing.delete(block); + block.i(local); + } + } + function mount_component(component, target, anchor, customElement) { + const { fragment, on_mount, on_destroy, after_update } = component.$$; + fragment && fragment.m(target, anchor); + if (!customElement) { + // onMount happens before the initial afterUpdate + add_render_callback(() => { + const new_on_destroy = on_mount.map(run).filter(is_function); + if (on_destroy) { + on_destroy.push(...new_on_destroy); + } + else { + // Edge case - component was destroyed immediately, + // most likely as a result of a binding initialising + run_all(new_on_destroy); + } + component.$$.on_mount = []; + }); + } + after_update.forEach(add_render_callback); + } + function destroy_component(component, detaching) { + const $$ = component.$$; + if ($$.fragment !== null) { + run_all($$.on_destroy); + $$.fragment && $$.fragment.d(detaching); + // TODO null out other refs, including component.$$ (but need to + // preserve final state?) + $$.on_destroy = $$.fragment = null; + $$.ctx = []; + } + } + function make_dirty(component, i) { + if (component.$$.dirty[0] === -1) { + dirty_components.push(component); + schedule_update(); + component.$$.dirty.fill(0); + } + component.$$.dirty[(i / 31) | 0] |= (1 << (i % 31)); + } + function init(component, options, instance, create_fragment, not_equal, props, append_styles, dirty = [-1]) { + const parent_component = current_component; + set_current_component(component); + const $$ = component.$$ = { + fragment: null, + ctx: null, + // state + props, + update: noop, + not_equal, + bound: blank_object(), + // lifecycle + on_mount: [], + on_destroy: [], + on_disconnect: [], + before_update: [], + after_update: [], + context: new Map(parent_component ? parent_component.$$.context : options.context || []), + // everything else + callbacks: blank_object(), + dirty, + skip_bound: false, + root: options.target || parent_component.$$.root + }; + append_styles && append_styles($$.root); + let ready = false; + $$.ctx = instance + ? instance(component, options.props || {}, (i, ret, ...rest) => { + const value = rest.length ? rest[0] : ret; + if ($$.ctx && not_equal($$.ctx[i], $$.ctx[i] = value)) { + if (!$$.skip_bound && $$.bound[i]) + $$.bound[i](value); + if (ready) + make_dirty(component, i); + } + return ret; + }) + : []; + $$.update(); + ready = true; + run_all($$.before_update); + // `false` as a special case of no DOM component + $$.fragment = create_fragment ? create_fragment($$.ctx) : false; + if (options.target) { + if (options.hydrate) { + const nodes = children(options.target); + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + $$.fragment && $$.fragment.l(nodes); + nodes.forEach(detach); + } + else { + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + $$.fragment && $$.fragment.c(); + } + if (options.intro) + transition_in(component.$$.fragment); + mount_component(component, options.target, options.anchor, options.customElement); + flush(); + } + set_current_component(parent_component); + } + /** + * Base class for Svelte components. Used when dev=false. + */ + class SvelteComponent { + $destroy() { + destroy_component(this, 1); + this.$destroy = noop; + } + $on(type, callback) { + const callbacks = (this.$$.callbacks[type] || (this.$$.callbacks[type] = [])); + callbacks.push(callback); + return () => { + const index = callbacks.indexOf(callback); + if (index !== -1) + callbacks.splice(index, 1); + }; + } + $set($$props) { + if (this.$$set && !is_empty($$props)) { + this.$$.skip_bound = true; + this.$$set($$props); + this.$$.skip_bound = false; + } + } + } + + function dispatch_dev(type, detail) { + document.dispatchEvent(custom_event(type, Object.assign({ version: '3.42.1' }, detail), true)); + } + function append_dev(target, node) { + dispatch_dev('SvelteDOMInsert', { target, node }); + append(target, node); + } + function insert_dev(target, node, anchor) { + dispatch_dev('SvelteDOMInsert', { target, node, anchor }); + insert(target, node, anchor); + } + function detach_dev(node) { + dispatch_dev('SvelteDOMRemove', { node }); + detach(node); + } + function listen_dev(node, event, handler, options, has_prevent_default, has_stop_propagation) { + const modifiers = options === true ? ['capture'] : options ? Array.from(Object.keys(options)) : []; + if (has_prevent_default) + modifiers.push('preventDefault'); + if (has_stop_propagation) + modifiers.push('stopPropagation'); + dispatch_dev('SvelteDOMAddEventListener', { node, event, handler, modifiers }); + const dispose = listen(node, event, handler, options); + return () => { + dispatch_dev('SvelteDOMRemoveEventListener', { node, event, handler, modifiers }); + dispose(); + }; + } + function attr_dev(node, attribute, value) { + attr(node, attribute, value); + if (value == null) + dispatch_dev('SvelteDOMRemoveAttribute', { node, attribute }); + else + dispatch_dev('SvelteDOMSetAttribute', { node, attribute, value }); + } + function prop_dev(node, property, value) { + node[property] = value; + dispatch_dev('SvelteDOMSetProperty', { node, property, value }); + } + function set_data_dev(text, data) { + data = '' + data; + if (text.wholeText === data) + return; + dispatch_dev('SvelteDOMSetData', { node: text, data }); + text.data = data; + } + function validate_each_argument(arg) { + if (typeof arg !== 'string' && !(arg && typeof arg === 'object' && 'length' in arg)) { + let msg = '{#each} only iterates over array-like objects.'; + if (typeof Symbol === 'function' && arg && Symbol.iterator in arg) { + msg += ' You can use a spread to convert this iterable into an array.'; + } + throw new Error(msg); + } + } + function validate_slots(name, slot, keys) { + for (const slot_key of Object.keys(slot)) { + if (!~keys.indexOf(slot_key)) { + console.warn(`<${name}> received an unexpected slot "${slot_key}".`); + } + } + } + /** + * Base class for Svelte components with some minor dev-enhancements. Used when dev=true. + */ + class SvelteComponentDev extends SvelteComponent { + constructor(options) { + if (!options || (!options.target && !options.$$inline)) { + throw new Error("'target' is a required option"); + } + super(); + } + $destroy() { + super.$destroy(); + this.$destroy = () => { + console.warn('Component was already destroyed'); // eslint-disable-line no-console + }; + } + $capture_state() { } + $inject_state() { } + } + + /* src/App.svelte generated by Svelte v3.42.1 */ + + const file = "src/App.svelte"; + + function get_each_context(ctx, list, i) { + const child_ctx = ctx.slice(); + child_ctx[15] = list[i]; + child_ctx[3] = i; + return child_ctx; + } + + // (56:1) {#each filteredPeople as person, i} + function create_each_block(ctx) { + let option; + let t0_value = /*person*/ ctx[15].last + ""; + let t0; + let t1; + let t2_value = /*person*/ ctx[15].first + ""; + let t2; + + const block = { + c: function create() { + option = element("option"); + t0 = text(t0_value); + t1 = text(", "); + t2 = text(t2_value); + option.__value = /*i*/ ctx[3]; + option.value = option.__value; + attr_dev(option, "class", "svelte-lxvsk7"); + add_location(option, file, 56, 2, 1258); + }, + m: function mount(target, anchor) { + insert_dev(target, option, anchor); + append_dev(option, t0); + append_dev(option, t1); + append_dev(option, t2); + }, + p: function update(ctx, dirty) { + if (dirty & /*filteredPeople*/ 2 && t0_value !== (t0_value = /*person*/ ctx[15].last + "")) set_data_dev(t0, t0_value); + if (dirty & /*filteredPeople*/ 2 && t2_value !== (t2_value = /*person*/ ctx[15].first + "")) set_data_dev(t2, t2_value); + }, + d: function destroy(detaching) { + if (detaching) detach_dev(option); + } + }; + + dispatch_dev("SvelteRegisterBlock", { + block, + id: create_each_block.name, + type: "each", + source: "(56:1) {#each filteredPeople as person, i}", + ctx + }); + + return block; + } + + function create_fragment(ctx) { + let input0; + let t0; + let select; + let t1; + let label0; + let input1; + let t2; + let label1; + let input2; + let t3; + let div; + let button0; + let t4; + let button0_disabled_value; + let t5; + let button1; + let t6; + let button1_disabled_value; + let t7; + let button2; + let t8; + let button2_disabled_value; + let mounted; + let dispose; + let each_value = /*filteredPeople*/ ctx[1]; + validate_each_argument(each_value); + let each_blocks = []; + + for (let i = 0; i < each_value.length; i += 1) { + each_blocks[i] = create_each_block(get_each_context(ctx, each_value, i)); + } + + const block = { + c: function create() { + input0 = element("input"); + t0 = space(); + select = element("select"); + + for (let i = 0; i < each_blocks.length; i += 1) { + each_blocks[i].c(); + } + + t1 = space(); + label0 = element("label"); + input1 = element("input"); + t2 = space(); + label1 = element("label"); + input2 = element("input"); + t3 = space(); + div = element("div"); + button0 = element("button"); + t4 = text("create"); + t5 = space(); + button1 = element("button"); + t6 = text("update"); + t7 = space(); + button2 = element("button"); + t8 = text("delete"); + attr_dev(input0, "placeholder", "filter prefix"); + attr_dev(input0, "class", "svelte-lxvsk7"); + add_location(input0, file, 52, 0, 1129); + attr_dev(select, "size", 5); + attr_dev(select, "class", "svelte-lxvsk7"); + if (/*i*/ ctx[3] === void 0) add_render_callback(() => /*select_change_handler*/ ctx[11].call(select)); + add_location(select, file, 54, 0, 1186); + attr_dev(input1, "placeholder", "first"); + attr_dev(input1, "class", "svelte-lxvsk7"); + add_location(input1, file, 60, 7, 1342); + attr_dev(label0, "class", "svelte-lxvsk7"); + add_location(label0, file, 60, 0, 1335); + attr_dev(input2, "placeholder", "last"); + attr_dev(input2, "class", "svelte-lxvsk7"); + add_location(input2, file, 61, 7, 1404); + attr_dev(label1, "class", "svelte-lxvsk7"); + add_location(label1, file, 61, 0, 1397); + button0.disabled = button0_disabled_value = !/*first*/ ctx[4] || !/*last*/ ctx[5]; + attr_dev(button0, "class", "svelte-lxvsk7"); + add_location(button0, file, 64, 1, 1481); + button1.disabled = button1_disabled_value = !/*first*/ ctx[4] || !/*last*/ ctx[5] || !/*selected*/ ctx[2]; + attr_dev(button1, "class", "svelte-lxvsk7"); + add_location(button1, file, 65, 1, 1553); + button2.disabled = button2_disabled_value = !/*selected*/ ctx[2]; + attr_dev(button2, "class", "svelte-lxvsk7"); + add_location(button2, file, 66, 1, 1638); + attr_dev(div, "class", "buttons svelte-lxvsk7"); + add_location(div, file, 63, 0, 1458); + }, + l: function claim(nodes) { + throw new Error("options.hydrate only works if the component was compiled with the `hydratable: true` option"); + }, + m: function mount(target, anchor) { + insert_dev(target, input0, anchor); + set_input_value(input0, /*prefix*/ ctx[0]); + insert_dev(target, t0, anchor); + insert_dev(target, select, anchor); + + for (let i = 0; i < each_blocks.length; i += 1) { + each_blocks[i].m(select, null); + } + + select_option(select, /*i*/ ctx[3]); + insert_dev(target, t1, anchor); + insert_dev(target, label0, anchor); + append_dev(label0, input1); + set_input_value(input1, /*first*/ ctx[4]); + insert_dev(target, t2, anchor); + insert_dev(target, label1, anchor); + append_dev(label1, input2); + set_input_value(input2, /*last*/ ctx[5]); + insert_dev(target, t3, anchor); + insert_dev(target, div, anchor); + append_dev(div, button0); + append_dev(button0, t4); + append_dev(div, t5); + append_dev(div, button1); + append_dev(button1, t6); + append_dev(div, t7); + append_dev(div, button2); + append_dev(button2, t8); + + if (!mounted) { + dispose = [ + listen_dev(input0, "input", /*input0_input_handler*/ ctx[10]), + listen_dev(select, "change", /*select_change_handler*/ ctx[11]), + listen_dev(input1, "input", /*input1_input_handler*/ ctx[12]), + listen_dev(input2, "input", /*input2_input_handler*/ ctx[13]), + listen_dev(button0, "click", /*create*/ ctx[6], false, false, false), + listen_dev(button1, "click", /*update*/ ctx[7], false, false, false), + listen_dev(button2, "click", /*remove*/ ctx[8], false, false, false) + ]; + + mounted = true; + } + }, + p: function update(ctx, [dirty]) { + if (dirty & /*prefix*/ 1 && input0.value !== /*prefix*/ ctx[0]) { + set_input_value(input0, /*prefix*/ ctx[0]); + } + + if (dirty & /*filteredPeople*/ 2) { + each_value = /*filteredPeople*/ ctx[1]; + validate_each_argument(each_value); + let i; + + for (i = 0; i < each_value.length; i += 1) { + const child_ctx = get_each_context(ctx, each_value, i); + + if (each_blocks[i]) { + each_blocks[i].p(child_ctx, dirty); + } else { + each_blocks[i] = create_each_block(child_ctx); + each_blocks[i].c(); + each_blocks[i].m(select, null); + } + } + + for (; i < each_blocks.length; i += 1) { + each_blocks[i].d(1); + } + + each_blocks.length = each_value.length; + } + + if (dirty & /*i*/ 8) { + select_option(select, /*i*/ ctx[3]); + } + + if (dirty & /*first*/ 16 && input1.value !== /*first*/ ctx[4]) { + set_input_value(input1, /*first*/ ctx[4]); + } + + if (dirty & /*last*/ 32 && input2.value !== /*last*/ ctx[5]) { + set_input_value(input2, /*last*/ ctx[5]); + } + + if (dirty & /*first, last*/ 48 && button0_disabled_value !== (button0_disabled_value = !/*first*/ ctx[4] || !/*last*/ ctx[5])) { + prop_dev(button0, "disabled", button0_disabled_value); + } + + if (dirty & /*first, last, selected*/ 52 && button1_disabled_value !== (button1_disabled_value = !/*first*/ ctx[4] || !/*last*/ ctx[5] || !/*selected*/ ctx[2])) { + prop_dev(button1, "disabled", button1_disabled_value); + } + + if (dirty & /*selected*/ 4 && button2_disabled_value !== (button2_disabled_value = !/*selected*/ ctx[2])) { + prop_dev(button2, "disabled", button2_disabled_value); + } + }, + i: noop, + o: noop, + d: function destroy(detaching) { + if (detaching) detach_dev(input0); + if (detaching) detach_dev(t0); + if (detaching) detach_dev(select); + destroy_each(each_blocks, detaching); + if (detaching) detach_dev(t1); + if (detaching) detach_dev(label0); + if (detaching) detach_dev(t2); + if (detaching) detach_dev(label1); + if (detaching) detach_dev(t3); + if (detaching) detach_dev(div); + mounted = false; + run_all(dispose); + } + }; + + dispatch_dev("SvelteRegisterBlock", { + block, + id: create_fragment.name, + type: "component", + source: "", + ctx + }); + + return block; + } + + function instance($$self, $$props, $$invalidate) { + let filteredPeople; + let selected; + let { $$slots: slots = {}, $$scope } = $$props; + validate_slots('App', slots, []); + + let people = [ + { first: 'Hans', last: 'Emil' }, + { first: 'Max', last: 'Mustermann' }, + { first: 'Roman', last: 'Tisch' } + ]; + + let prefix = ''; + let first = ''; + let last = ''; + let i = 0; + + function create() { + $$invalidate(9, people = people.concat({ first, last })); + $$invalidate(3, i = people.length - 1); + $$invalidate(4, first = $$invalidate(5, last = '')); + } + + function update() { + $$invalidate(2, selected.first = first, selected); + $$invalidate(2, selected.last = last, selected); + $$invalidate(9, people); + } + + function remove() { + // Remove selected person from the source array (people), not the filtered array + const index = people.indexOf(selected); + + $$invalidate(9, people = [...people.slice(0, index), ...people.slice(index + 1)]); + $$invalidate(4, first = $$invalidate(5, last = '')); + $$invalidate(3, i = Math.min(i, filteredPeople.length - 2)); + } + + function reset_inputs(person) { + $$invalidate(4, first = person ? person.first : ''); + $$invalidate(5, last = person ? person.last : ''); + } + + const writable_props = []; + + Object.keys($$props).forEach(key => { + if (!~writable_props.indexOf(key) && key.slice(0, 2) !== '$$' && key !== 'slot') console.warn(` was created with unknown prop '${key}'`); + }); + + function input0_input_handler() { + prefix = this.value; + $$invalidate(0, prefix); + } + + function select_change_handler() { + i = select_value(this); + $$invalidate(3, i); + } + + function input1_input_handler() { + first = this.value; + $$invalidate(4, first); + } + + function input2_input_handler() { + last = this.value; + $$invalidate(5, last); + } + + $$self.$capture_state = () => ({ + people, + prefix, + first, + last, + i, + create, + update, + remove, + reset_inputs, + filteredPeople, + selected + }); + + $$self.$inject_state = $$props => { + if ('people' in $$props) $$invalidate(9, people = $$props.people); + if ('prefix' in $$props) $$invalidate(0, prefix = $$props.prefix); + if ('first' in $$props) $$invalidate(4, first = $$props.first); + if ('last' in $$props) $$invalidate(5, last = $$props.last); + if ('i' in $$props) $$invalidate(3, i = $$props.i); + if ('filteredPeople' in $$props) $$invalidate(1, filteredPeople = $$props.filteredPeople); + if ('selected' in $$props) $$invalidate(2, selected = $$props.selected); + }; + + if ($$props && "$$inject" in $$props) { + $$self.$inject_state($$props.$$inject); + } + + $$self.$$.update = () => { + if ($$self.$$.dirty & /*prefix, people*/ 513) { + $$invalidate(1, filteredPeople = prefix + ? people.filter(person => { + const name = `${person.last}, ${person.first}`; + return name.toLowerCase().startsWith(prefix.toLowerCase()); + }) + : people); + } + + if ($$self.$$.dirty & /*filteredPeople, i*/ 10) { + $$invalidate(2, selected = filteredPeople[i]); + } + + if ($$self.$$.dirty & /*selected*/ 4) { + reset_inputs(selected); + } + }; + + return [ + prefix, + filteredPeople, + selected, + i, + first, + last, + create, + update, + remove, + people, + input0_input_handler, + select_change_handler, + input1_input_handler, + input2_input_handler + ]; + } + + class App extends SvelteComponentDev { + constructor(options) { + super(options); + init(this, options, instance, create_fragment, safe_not_equal, {}); + + dispatch_dev("SvelteRegisterComponent", { + component: this, + tagName: "App", + options, + id: create_fragment.name + }); + } + } + + var app = new App({ + target: document.body + }); + + return app; + +}()); +//# sourceMappingURL=bundle.js.map diff --git a/frontend/public/build/bundle.js.map b/frontend/public/build/bundle.js.map new file mode 100644 index 0000000..6c892b7 --- /dev/null +++ b/frontend/public/build/bundle.js.map @@ -0,0 +1 @@ +{"version":3,"file":"bundle.js","sources":["../../node_modules/.pnpm/svelte@3.42.1/node_modules/svelte/internal/index.mjs","../../src/App.svelte","../../src/main.js"],"sourcesContent":["function noop() { }\nconst identity = x => x;\nfunction assign(tar, src) {\n // @ts-ignore\n for (const k in src)\n tar[k] = src[k];\n return tar;\n}\nfunction is_promise(value) {\n return value && typeof value === 'object' && typeof value.then === 'function';\n}\nfunction add_location(element, file, line, column, char) {\n element.__svelte_meta = {\n loc: { file, line, column, char }\n };\n}\nfunction run(fn) {\n return fn();\n}\nfunction blank_object() {\n return Object.create(null);\n}\nfunction run_all(fns) {\n fns.forEach(run);\n}\nfunction is_function(thing) {\n return typeof thing === 'function';\n}\nfunction safe_not_equal(a, b) {\n return a != a ? b == b : a !== b || ((a && typeof a === 'object') || typeof a === 'function');\n}\nlet src_url_equal_anchor;\nfunction src_url_equal(element_src, url) {\n if (!src_url_equal_anchor) {\n src_url_equal_anchor = document.createElement('a');\n }\n src_url_equal_anchor.href = url;\n return element_src === src_url_equal_anchor.href;\n}\nfunction not_equal(a, b) {\n return a != a ? b == b : a !== b;\n}\nfunction is_empty(obj) {\n return Object.keys(obj).length === 0;\n}\nfunction validate_store(store, name) {\n if (store != null && typeof store.subscribe !== 'function') {\n throw new Error(`'${name}' is not a store with a 'subscribe' method`);\n }\n}\nfunction subscribe(store, ...callbacks) {\n if (store == null) {\n return noop;\n }\n const unsub = store.subscribe(...callbacks);\n return unsub.unsubscribe ? () => unsub.unsubscribe() : unsub;\n}\nfunction get_store_value(store) {\n let value;\n subscribe(store, _ => value = _)();\n return value;\n}\nfunction component_subscribe(component, store, callback) {\n component.$$.on_destroy.push(subscribe(store, callback));\n}\nfunction create_slot(definition, ctx, $$scope, fn) {\n if (definition) {\n const slot_ctx = get_slot_context(definition, ctx, $$scope, fn);\n return definition[0](slot_ctx);\n }\n}\nfunction get_slot_context(definition, ctx, $$scope, fn) {\n return definition[1] && fn\n ? assign($$scope.ctx.slice(), definition[1](fn(ctx)))\n : $$scope.ctx;\n}\nfunction get_slot_changes(definition, $$scope, dirty, fn) {\n if (definition[2] && fn) {\n const lets = definition[2](fn(dirty));\n if ($$scope.dirty === undefined) {\n return lets;\n }\n if (typeof lets === 'object') {\n const merged = [];\n const len = Math.max($$scope.dirty.length, lets.length);\n for (let i = 0; i < len; i += 1) {\n merged[i] = $$scope.dirty[i] | lets[i];\n }\n return merged;\n }\n return $$scope.dirty | lets;\n }\n return $$scope.dirty;\n}\nfunction update_slot_base(slot, slot_definition, ctx, $$scope, slot_changes, get_slot_context_fn) {\n if (slot_changes) {\n const slot_context = get_slot_context(slot_definition, ctx, $$scope, get_slot_context_fn);\n slot.p(slot_context, slot_changes);\n }\n}\nfunction update_slot(slot, slot_definition, ctx, $$scope, dirty, get_slot_changes_fn, get_slot_context_fn) {\n const slot_changes = get_slot_changes(slot_definition, $$scope, dirty, get_slot_changes_fn);\n update_slot_base(slot, slot_definition, ctx, $$scope, slot_changes, get_slot_context_fn);\n}\nfunction get_all_dirty_from_scope($$scope) {\n if ($$scope.ctx.length > 32) {\n const dirty = [];\n const length = $$scope.ctx.length / 32;\n for (let i = 0; i < length; i++) {\n dirty[i] = -1;\n }\n return dirty;\n }\n return -1;\n}\nfunction exclude_internal_props(props) {\n const result = {};\n for (const k in props)\n if (k[0] !== '$')\n result[k] = props[k];\n return result;\n}\nfunction compute_rest_props(props, keys) {\n const rest = {};\n keys = new Set(keys);\n for (const k in props)\n if (!keys.has(k) && k[0] !== '$')\n rest[k] = props[k];\n return rest;\n}\nfunction compute_slots(slots) {\n const result = {};\n for (const key in slots) {\n result[key] = true;\n }\n return result;\n}\nfunction once(fn) {\n let ran = false;\n return function (...args) {\n if (ran)\n return;\n ran = true;\n fn.call(this, ...args);\n };\n}\nfunction null_to_empty(value) {\n return value == null ? '' : value;\n}\nfunction set_store_value(store, ret, value) {\n store.set(value);\n return ret;\n}\nconst has_prop = (obj, prop) => Object.prototype.hasOwnProperty.call(obj, prop);\nfunction action_destroyer(action_result) {\n return action_result && is_function(action_result.destroy) ? action_result.destroy : noop;\n}\n\nconst is_client = typeof window !== 'undefined';\nlet now = is_client\n ? () => window.performance.now()\n : () => Date.now();\nlet raf = is_client ? cb => requestAnimationFrame(cb) : noop;\n// used internally for testing\nfunction set_now(fn) {\n now = fn;\n}\nfunction set_raf(fn) {\n raf = fn;\n}\n\nconst tasks = new Set();\nfunction run_tasks(now) {\n tasks.forEach(task => {\n if (!task.c(now)) {\n tasks.delete(task);\n task.f();\n }\n });\n if (tasks.size !== 0)\n raf(run_tasks);\n}\n/**\n * For testing purposes only!\n */\nfunction clear_loops() {\n tasks.clear();\n}\n/**\n * Creates a new task that runs on each raf frame\n * until it returns a falsy value or is aborted\n */\nfunction loop(callback) {\n let task;\n if (tasks.size === 0)\n raf(run_tasks);\n return {\n promise: new Promise(fulfill => {\n tasks.add(task = { c: callback, f: fulfill });\n }),\n abort() {\n tasks.delete(task);\n }\n };\n}\n\n// Track which nodes are claimed during hydration. Unclaimed nodes can then be removed from the DOM\n// at the end of hydration without touching the remaining nodes.\nlet is_hydrating = false;\nfunction start_hydrating() {\n is_hydrating = true;\n}\nfunction end_hydrating() {\n is_hydrating = false;\n}\nfunction upper_bound(low, high, key, value) {\n // Return first index of value larger than input value in the range [low, high)\n while (low < high) {\n const mid = low + ((high - low) >> 1);\n if (key(mid) <= value) {\n low = mid + 1;\n }\n else {\n high = mid;\n }\n }\n return low;\n}\nfunction init_hydrate(target) {\n if (target.hydrate_init)\n return;\n target.hydrate_init = true;\n // We know that all children have claim_order values since the unclaimed have been detached if target is not \n let children = target.childNodes;\n // If target is , there may be children without claim_order\n if (target.nodeName === 'HEAD') {\n const myChildren = [];\n for (let i = 0; i < children.length; i++) {\n const node = children[i];\n if (node.claim_order !== undefined) {\n myChildren.push(node);\n }\n }\n children = myChildren;\n }\n /*\n * Reorder claimed children optimally.\n * We can reorder claimed children optimally by finding the longest subsequence of\n * nodes that are already claimed in order and only moving the rest. The longest\n * subsequence subsequence of nodes that are claimed in order can be found by\n * computing the longest increasing subsequence of .claim_order values.\n *\n * This algorithm is optimal in generating the least amount of reorder operations\n * possible.\n *\n * Proof:\n * We know that, given a set of reordering operations, the nodes that do not move\n * always form an increasing subsequence, since they do not move among each other\n * meaning that they must be already ordered among each other. Thus, the maximal\n * set of nodes that do not move form a longest increasing subsequence.\n */\n // Compute longest increasing subsequence\n // m: subsequence length j => index k of smallest value that ends an increasing subsequence of length j\n const m = new Int32Array(children.length + 1);\n // Predecessor indices + 1\n const p = new Int32Array(children.length);\n m[0] = -1;\n let longest = 0;\n for (let i = 0; i < children.length; i++) {\n const current = children[i].claim_order;\n // Find the largest subsequence length such that it ends in a value less than our current value\n // upper_bound returns first greater value, so we subtract one\n // with fast path for when we are on the current longest subsequence\n const seqLen = ((longest > 0 && children[m[longest]].claim_order <= current) ? longest + 1 : upper_bound(1, longest, idx => children[m[idx]].claim_order, current)) - 1;\n p[i] = m[seqLen] + 1;\n const newLen = seqLen + 1;\n // We can guarantee that current is the smallest value. Otherwise, we would have generated a longer sequence.\n m[newLen] = i;\n longest = Math.max(newLen, longest);\n }\n // The longest increasing subsequence of nodes (initially reversed)\n const lis = [];\n // The rest of the nodes, nodes that will be moved\n const toMove = [];\n let last = children.length - 1;\n for (let cur = m[longest] + 1; cur != 0; cur = p[cur - 1]) {\n lis.push(children[cur - 1]);\n for (; last >= cur; last--) {\n toMove.push(children[last]);\n }\n last--;\n }\n for (; last >= 0; last--) {\n toMove.push(children[last]);\n }\n lis.reverse();\n // We sort the nodes being moved to guarantee that their insertion order matches the claim order\n toMove.sort((a, b) => a.claim_order - b.claim_order);\n // Finally, we move the nodes\n for (let i = 0, j = 0; i < toMove.length; i++) {\n while (j < lis.length && toMove[i].claim_order >= lis[j].claim_order) {\n j++;\n }\n const anchor = j < lis.length ? lis[j] : null;\n target.insertBefore(toMove[i], anchor);\n }\n}\nfunction append(target, node) {\n target.appendChild(node);\n}\nfunction append_styles(target, style_sheet_id, styles) {\n const append_styles_to = get_root_for_style(target);\n if (!append_styles_to.getElementById(style_sheet_id)) {\n const style = element('style');\n style.id = style_sheet_id;\n style.textContent = styles;\n append_stylesheet(append_styles_to, style);\n }\n}\nfunction get_root_for_style(node) {\n if (!node)\n return document;\n const root = node.getRootNode ? node.getRootNode() : node.ownerDocument;\n if (root.host) {\n return root;\n }\n return document;\n}\nfunction append_empty_stylesheet(node) {\n const style_element = element('style');\n append_stylesheet(get_root_for_style(node), style_element);\n return style_element;\n}\nfunction append_stylesheet(node, style) {\n append(node.head || node, style);\n}\nfunction append_hydration(target, node) {\n if (is_hydrating) {\n init_hydrate(target);\n if ((target.actual_end_child === undefined) || ((target.actual_end_child !== null) && (target.actual_end_child.parentElement !== target))) {\n target.actual_end_child = target.firstChild;\n }\n // Skip nodes of undefined ordering\n while ((target.actual_end_child !== null) && (target.actual_end_child.claim_order === undefined)) {\n target.actual_end_child = target.actual_end_child.nextSibling;\n }\n if (node !== target.actual_end_child) {\n // We only insert if the ordering of this node should be modified or the parent node is not target\n if (node.claim_order !== undefined || node.parentNode !== target) {\n target.insertBefore(node, target.actual_end_child);\n }\n }\n else {\n target.actual_end_child = node.nextSibling;\n }\n }\n else if (node.parentNode !== target || node.nextSibling !== null) {\n target.appendChild(node);\n }\n}\nfunction insert(target, node, anchor) {\n target.insertBefore(node, anchor || null);\n}\nfunction insert_hydration(target, node, anchor) {\n if (is_hydrating && !anchor) {\n append_hydration(target, node);\n }\n else if (node.parentNode !== target || node.nextSibling != anchor) {\n target.insertBefore(node, anchor || null);\n }\n}\nfunction detach(node) {\n node.parentNode.removeChild(node);\n}\nfunction destroy_each(iterations, detaching) {\n for (let i = 0; i < iterations.length; i += 1) {\n if (iterations[i])\n iterations[i].d(detaching);\n }\n}\nfunction element(name) {\n return document.createElement(name);\n}\nfunction element_is(name, is) {\n return document.createElement(name, { is });\n}\nfunction object_without_properties(obj, exclude) {\n const target = {};\n for (const k in obj) {\n if (has_prop(obj, k)\n // @ts-ignore\n && exclude.indexOf(k) === -1) {\n // @ts-ignore\n target[k] = obj[k];\n }\n }\n return target;\n}\nfunction svg_element(name) {\n return document.createElementNS('http://www.w3.org/2000/svg', name);\n}\nfunction text(data) {\n return document.createTextNode(data);\n}\nfunction space() {\n return text(' ');\n}\nfunction empty() {\n return text('');\n}\nfunction listen(node, event, handler, options) {\n node.addEventListener(event, handler, options);\n return () => node.removeEventListener(event, handler, options);\n}\nfunction prevent_default(fn) {\n return function (event) {\n event.preventDefault();\n // @ts-ignore\n return fn.call(this, event);\n };\n}\nfunction stop_propagation(fn) {\n return function (event) {\n event.stopPropagation();\n // @ts-ignore\n return fn.call(this, event);\n };\n}\nfunction self(fn) {\n return function (event) {\n // @ts-ignore\n if (event.target === this)\n fn.call(this, event);\n };\n}\nfunction trusted(fn) {\n return function (event) {\n // @ts-ignore\n if (event.isTrusted)\n fn.call(this, event);\n };\n}\nfunction attr(node, attribute, value) {\n if (value == null)\n node.removeAttribute(attribute);\n else if (node.getAttribute(attribute) !== value)\n node.setAttribute(attribute, value);\n}\nfunction set_attributes(node, attributes) {\n // @ts-ignore\n const descriptors = Object.getOwnPropertyDescriptors(node.__proto__);\n for (const key in attributes) {\n if (attributes[key] == null) {\n node.removeAttribute(key);\n }\n else if (key === 'style') {\n node.style.cssText = attributes[key];\n }\n else if (key === '__value') {\n node.value = node[key] = attributes[key];\n }\n else if (descriptors[key] && descriptors[key].set) {\n node[key] = attributes[key];\n }\n else {\n attr(node, key, attributes[key]);\n }\n }\n}\nfunction set_svg_attributes(node, attributes) {\n for (const key in attributes) {\n attr(node, key, attributes[key]);\n }\n}\nfunction set_custom_element_data(node, prop, value) {\n if (prop in node) {\n node[prop] = typeof node[prop] === 'boolean' && value === '' ? true : value;\n }\n else {\n attr(node, prop, value);\n }\n}\nfunction xlink_attr(node, attribute, value) {\n node.setAttributeNS('http://www.w3.org/1999/xlink', attribute, value);\n}\nfunction get_binding_group_value(group, __value, checked) {\n const value = new Set();\n for (let i = 0; i < group.length; i += 1) {\n if (group[i].checked)\n value.add(group[i].__value);\n }\n if (!checked) {\n value.delete(__value);\n }\n return Array.from(value);\n}\nfunction to_number(value) {\n return value === '' ? null : +value;\n}\nfunction time_ranges_to_array(ranges) {\n const array = [];\n for (let i = 0; i < ranges.length; i += 1) {\n array.push({ start: ranges.start(i), end: ranges.end(i) });\n }\n return array;\n}\nfunction children(element) {\n return Array.from(element.childNodes);\n}\nfunction init_claim_info(nodes) {\n if (nodes.claim_info === undefined) {\n nodes.claim_info = { last_index: 0, total_claimed: 0 };\n }\n}\nfunction claim_node(nodes, predicate, processNode, createNode, dontUpdateLastIndex = false) {\n // Try to find nodes in an order such that we lengthen the longest increasing subsequence\n init_claim_info(nodes);\n const resultNode = (() => {\n // We first try to find an element after the previous one\n for (let i = nodes.claim_info.last_index; i < nodes.length; i++) {\n const node = nodes[i];\n if (predicate(node)) {\n const replacement = processNode(node);\n if (replacement === undefined) {\n nodes.splice(i, 1);\n }\n else {\n nodes[i] = replacement;\n }\n if (!dontUpdateLastIndex) {\n nodes.claim_info.last_index = i;\n }\n return node;\n }\n }\n // Otherwise, we try to find one before\n // We iterate in reverse so that we don't go too far back\n for (let i = nodes.claim_info.last_index - 1; i >= 0; i--) {\n const node = nodes[i];\n if (predicate(node)) {\n const replacement = processNode(node);\n if (replacement === undefined) {\n nodes.splice(i, 1);\n }\n else {\n nodes[i] = replacement;\n }\n if (!dontUpdateLastIndex) {\n nodes.claim_info.last_index = i;\n }\n else if (replacement === undefined) {\n // Since we spliced before the last_index, we decrease it\n nodes.claim_info.last_index--;\n }\n return node;\n }\n }\n // If we can't find any matching node, we create a new one\n return createNode();\n })();\n resultNode.claim_order = nodes.claim_info.total_claimed;\n nodes.claim_info.total_claimed += 1;\n return resultNode;\n}\nfunction claim_element(nodes, name, attributes, svg) {\n return claim_node(nodes, (node) => node.nodeName === name, (node) => {\n const remove = [];\n for (let j = 0; j < node.attributes.length; j++) {\n const attribute = node.attributes[j];\n if (!attributes[attribute.name]) {\n remove.push(attribute.name);\n }\n }\n remove.forEach(v => node.removeAttribute(v));\n return undefined;\n }, () => svg ? svg_element(name) : element(name));\n}\nfunction claim_text(nodes, data) {\n return claim_node(nodes, (node) => node.nodeType === 3, (node) => {\n const dataStr = '' + data;\n if (node.data.startsWith(dataStr)) {\n if (node.data.length !== dataStr.length) {\n return node.splitText(dataStr.length);\n }\n }\n else {\n node.data = dataStr;\n }\n }, () => text(data), true // Text nodes should not update last index since it is likely not worth it to eliminate an increasing subsequence of actual elements\n );\n}\nfunction claim_space(nodes) {\n return claim_text(nodes, ' ');\n}\nfunction find_comment(nodes, text, start) {\n for (let i = start; i < nodes.length; i += 1) {\n const node = nodes[i];\n if (node.nodeType === 8 /* comment node */ && node.textContent.trim() === text) {\n return i;\n }\n }\n return nodes.length;\n}\nfunction claim_html_tag(nodes) {\n // find html opening tag\n const start_index = find_comment(nodes, 'HTML_TAG_START', 0);\n const end_index = find_comment(nodes, 'HTML_TAG_END', start_index);\n if (start_index === end_index) {\n return new HtmlTagHydration();\n }\n init_claim_info(nodes);\n const html_tag_nodes = nodes.splice(start_index, end_index + 1);\n detach(html_tag_nodes[0]);\n detach(html_tag_nodes[html_tag_nodes.length - 1]);\n const claimed_nodes = html_tag_nodes.slice(1, html_tag_nodes.length - 1);\n for (const n of claimed_nodes) {\n n.claim_order = nodes.claim_info.total_claimed;\n nodes.claim_info.total_claimed += 1;\n }\n return new HtmlTagHydration(claimed_nodes);\n}\nfunction set_data(text, data) {\n data = '' + data;\n if (text.wholeText !== data)\n text.data = data;\n}\nfunction set_input_value(input, value) {\n input.value = value == null ? '' : value;\n}\nfunction set_input_type(input, type) {\n try {\n input.type = type;\n }\n catch (e) {\n // do nothing\n }\n}\nfunction set_style(node, key, value, important) {\n node.style.setProperty(key, value, important ? 'important' : '');\n}\nfunction select_option(select, value) {\n for (let i = 0; i < select.options.length; i += 1) {\n const option = select.options[i];\n if (option.__value === value) {\n option.selected = true;\n return;\n }\n }\n}\nfunction select_options(select, value) {\n for (let i = 0; i < select.options.length; i += 1) {\n const option = select.options[i];\n option.selected = ~value.indexOf(option.__value);\n }\n}\nfunction select_value(select) {\n const selected_option = select.querySelector(':checked') || select.options[0];\n return selected_option && selected_option.__value;\n}\nfunction select_multiple_value(select) {\n return [].map.call(select.querySelectorAll(':checked'), option => option.__value);\n}\n// unfortunately this can't be a constant as that wouldn't be tree-shakeable\n// so we cache the result instead\nlet crossorigin;\nfunction is_crossorigin() {\n if (crossorigin === undefined) {\n crossorigin = false;\n try {\n if (typeof window !== 'undefined' && window.parent) {\n void window.parent.document;\n }\n }\n catch (error) {\n crossorigin = true;\n }\n }\n return crossorigin;\n}\nfunction add_resize_listener(node, fn) {\n const computed_style = getComputedStyle(node);\n if (computed_style.position === 'static') {\n node.style.position = 'relative';\n }\n const iframe = element('iframe');\n iframe.setAttribute('style', 'display: block; position: absolute; top: 0; left: 0; width: 100%; height: 100%; ' +\n 'overflow: hidden; border: 0; opacity: 0; pointer-events: none; z-index: -1;');\n iframe.setAttribute('aria-hidden', 'true');\n iframe.tabIndex = -1;\n const crossorigin = is_crossorigin();\n let unsubscribe;\n if (crossorigin) {\n iframe.src = \"data:text/html,\";\n unsubscribe = listen(window, 'message', (event) => {\n if (event.source === iframe.contentWindow)\n fn();\n });\n }\n else {\n iframe.src = 'about:blank';\n iframe.onload = () => {\n unsubscribe = listen(iframe.contentWindow, 'resize', fn);\n };\n }\n append(node, iframe);\n return () => {\n if (crossorigin) {\n unsubscribe();\n }\n else if (unsubscribe && iframe.contentWindow) {\n unsubscribe();\n }\n detach(iframe);\n };\n}\nfunction toggle_class(element, name, toggle) {\n element.classList[toggle ? 'add' : 'remove'](name);\n}\nfunction custom_event(type, detail, bubbles = false) {\n const e = document.createEvent('CustomEvent');\n e.initCustomEvent(type, bubbles, false, detail);\n return e;\n}\nfunction query_selector_all(selector, parent = document.body) {\n return Array.from(parent.querySelectorAll(selector));\n}\nclass HtmlTag {\n constructor() {\n this.e = this.n = null;\n }\n c(html) {\n this.h(html);\n }\n m(html, target, anchor = null) {\n if (!this.e) {\n this.e = element(target.nodeName);\n this.t = target;\n this.c(html);\n }\n this.i(anchor);\n }\n h(html) {\n this.e.innerHTML = html;\n this.n = Array.from(this.e.childNodes);\n }\n i(anchor) {\n for (let i = 0; i < this.n.length; i += 1) {\n insert(this.t, this.n[i], anchor);\n }\n }\n p(html) {\n this.d();\n this.h(html);\n this.i(this.a);\n }\n d() {\n this.n.forEach(detach);\n }\n}\nclass HtmlTagHydration extends HtmlTag {\n constructor(claimed_nodes) {\n super();\n this.e = this.n = null;\n this.l = claimed_nodes;\n }\n c(html) {\n if (this.l) {\n this.n = this.l;\n }\n else {\n super.c(html);\n }\n }\n i(anchor) {\n for (let i = 0; i < this.n.length; i += 1) {\n insert_hydration(this.t, this.n[i], anchor);\n }\n }\n}\nfunction attribute_to_object(attributes) {\n const result = {};\n for (const attribute of attributes) {\n result[attribute.name] = attribute.value;\n }\n return result;\n}\nfunction get_custom_elements_slots(element) {\n const result = {};\n element.childNodes.forEach((node) => {\n result[node.slot || 'default'] = true;\n });\n return result;\n}\n\nconst active_docs = new Set();\nlet active = 0;\n// https://github.com/darkskyapp/string-hash/blob/master/index.js\nfunction hash(str) {\n let hash = 5381;\n let i = str.length;\n while (i--)\n hash = ((hash << 5) - hash) ^ str.charCodeAt(i);\n return hash >>> 0;\n}\nfunction create_rule(node, a, b, duration, delay, ease, fn, uid = 0) {\n const step = 16.666 / duration;\n let keyframes = '{\\n';\n for (let p = 0; p <= 1; p += step) {\n const t = a + (b - a) * ease(p);\n keyframes += p * 100 + `%{${fn(t, 1 - t)}}\\n`;\n }\n const rule = keyframes + `100% {${fn(b, 1 - b)}}\\n}`;\n const name = `__svelte_${hash(rule)}_${uid}`;\n const doc = get_root_for_style(node);\n active_docs.add(doc);\n const stylesheet = doc.__svelte_stylesheet || (doc.__svelte_stylesheet = append_empty_stylesheet(node).sheet);\n const current_rules = doc.__svelte_rules || (doc.__svelte_rules = {});\n if (!current_rules[name]) {\n current_rules[name] = true;\n stylesheet.insertRule(`@keyframes ${name} ${rule}`, stylesheet.cssRules.length);\n }\n const animation = node.style.animation || '';\n node.style.animation = `${animation ? `${animation}, ` : ''}${name} ${duration}ms linear ${delay}ms 1 both`;\n active += 1;\n return name;\n}\nfunction delete_rule(node, name) {\n const previous = (node.style.animation || '').split(', ');\n const next = previous.filter(name\n ? anim => anim.indexOf(name) < 0 // remove specific animation\n : anim => anim.indexOf('__svelte') === -1 // remove all Svelte animations\n );\n const deleted = previous.length - next.length;\n if (deleted) {\n node.style.animation = next.join(', ');\n active -= deleted;\n if (!active)\n clear_rules();\n }\n}\nfunction clear_rules() {\n raf(() => {\n if (active)\n return;\n active_docs.forEach(doc => {\n const stylesheet = doc.__svelte_stylesheet;\n let i = stylesheet.cssRules.length;\n while (i--)\n stylesheet.deleteRule(i);\n doc.__svelte_rules = {};\n });\n active_docs.clear();\n });\n}\n\nfunction create_animation(node, from, fn, params) {\n if (!from)\n return noop;\n const to = node.getBoundingClientRect();\n if (from.left === to.left && from.right === to.right && from.top === to.top && from.bottom === to.bottom)\n return noop;\n const { delay = 0, duration = 300, easing = identity, \n // @ts-ignore todo: should this be separated from destructuring? Or start/end added to public api and documentation?\n start: start_time = now() + delay, \n // @ts-ignore todo:\n end = start_time + duration, tick = noop, css } = fn(node, { from, to }, params);\n let running = true;\n let started = false;\n let name;\n function start() {\n if (css) {\n name = create_rule(node, 0, 1, duration, delay, easing, css);\n }\n if (!delay) {\n started = true;\n }\n }\n function stop() {\n if (css)\n delete_rule(node, name);\n running = false;\n }\n loop(now => {\n if (!started && now >= start_time) {\n started = true;\n }\n if (started && now >= end) {\n tick(1, 0);\n stop();\n }\n if (!running) {\n return false;\n }\n if (started) {\n const p = now - start_time;\n const t = 0 + 1 * easing(p / duration);\n tick(t, 1 - t);\n }\n return true;\n });\n start();\n tick(0, 1);\n return stop;\n}\nfunction fix_position(node) {\n const style = getComputedStyle(node);\n if (style.position !== 'absolute' && style.position !== 'fixed') {\n const { width, height } = style;\n const a = node.getBoundingClientRect();\n node.style.position = 'absolute';\n node.style.width = width;\n node.style.height = height;\n add_transform(node, a);\n }\n}\nfunction add_transform(node, a) {\n const b = node.getBoundingClientRect();\n if (a.left !== b.left || a.top !== b.top) {\n const style = getComputedStyle(node);\n const transform = style.transform === 'none' ? '' : style.transform;\n node.style.transform = `${transform} translate(${a.left - b.left}px, ${a.top - b.top}px)`;\n }\n}\n\nlet current_component;\nfunction set_current_component(component) {\n current_component = component;\n}\nfunction get_current_component() {\n if (!current_component)\n throw new Error('Function called outside component initialization');\n return current_component;\n}\nfunction beforeUpdate(fn) {\n get_current_component().$$.before_update.push(fn);\n}\nfunction onMount(fn) {\n get_current_component().$$.on_mount.push(fn);\n}\nfunction afterUpdate(fn) {\n get_current_component().$$.after_update.push(fn);\n}\nfunction onDestroy(fn) {\n get_current_component().$$.on_destroy.push(fn);\n}\nfunction createEventDispatcher() {\n const component = get_current_component();\n return (type, detail) => {\n const callbacks = component.$$.callbacks[type];\n if (callbacks) {\n // TODO are there situations where events could be dispatched\n // in a server (non-DOM) environment?\n const event = custom_event(type, detail);\n callbacks.slice().forEach(fn => {\n fn.call(component, event);\n });\n }\n };\n}\nfunction setContext(key, context) {\n get_current_component().$$.context.set(key, context);\n}\nfunction getContext(key) {\n return get_current_component().$$.context.get(key);\n}\nfunction getAllContexts() {\n return get_current_component().$$.context;\n}\nfunction hasContext(key) {\n return get_current_component().$$.context.has(key);\n}\n// TODO figure out if we still want to support\n// shorthand events, or if we want to implement\n// a real bubbling mechanism\nfunction bubble(component, event) {\n const callbacks = component.$$.callbacks[event.type];\n if (callbacks) {\n // @ts-ignore\n callbacks.slice().forEach(fn => fn.call(this, event));\n }\n}\n\nconst dirty_components = [];\nconst intros = { enabled: false };\nconst binding_callbacks = [];\nconst render_callbacks = [];\nconst flush_callbacks = [];\nconst resolved_promise = Promise.resolve();\nlet update_scheduled = false;\nfunction schedule_update() {\n if (!update_scheduled) {\n update_scheduled = true;\n resolved_promise.then(flush);\n }\n}\nfunction tick() {\n schedule_update();\n return resolved_promise;\n}\nfunction add_render_callback(fn) {\n render_callbacks.push(fn);\n}\nfunction add_flush_callback(fn) {\n flush_callbacks.push(fn);\n}\nlet flushing = false;\nconst seen_callbacks = new Set();\nfunction flush() {\n if (flushing)\n return;\n flushing = true;\n do {\n // first, call beforeUpdate functions\n // and update components\n for (let i = 0; i < dirty_components.length; i += 1) {\n const component = dirty_components[i];\n set_current_component(component);\n update(component.$$);\n }\n set_current_component(null);\n dirty_components.length = 0;\n while (binding_callbacks.length)\n binding_callbacks.pop()();\n // then, once components are updated, call\n // afterUpdate functions. This may cause\n // subsequent updates...\n for (let i = 0; i < render_callbacks.length; i += 1) {\n const callback = render_callbacks[i];\n if (!seen_callbacks.has(callback)) {\n // ...so guard against infinite loops\n seen_callbacks.add(callback);\n callback();\n }\n }\n render_callbacks.length = 0;\n } while (dirty_components.length);\n while (flush_callbacks.length) {\n flush_callbacks.pop()();\n }\n update_scheduled = false;\n flushing = false;\n seen_callbacks.clear();\n}\nfunction update($$) {\n if ($$.fragment !== null) {\n $$.update();\n run_all($$.before_update);\n const dirty = $$.dirty;\n $$.dirty = [-1];\n $$.fragment && $$.fragment.p($$.ctx, dirty);\n $$.after_update.forEach(add_render_callback);\n }\n}\n\nlet promise;\nfunction wait() {\n if (!promise) {\n promise = Promise.resolve();\n promise.then(() => {\n promise = null;\n });\n }\n return promise;\n}\nfunction dispatch(node, direction, kind) {\n node.dispatchEvent(custom_event(`${direction ? 'intro' : 'outro'}${kind}`));\n}\nconst outroing = new Set();\nlet outros;\nfunction group_outros() {\n outros = {\n r: 0,\n c: [],\n p: outros // parent group\n };\n}\nfunction check_outros() {\n if (!outros.r) {\n run_all(outros.c);\n }\n outros = outros.p;\n}\nfunction transition_in(block, local) {\n if (block && block.i) {\n outroing.delete(block);\n block.i(local);\n }\n}\nfunction transition_out(block, local, detach, callback) {\n if (block && block.o) {\n if (outroing.has(block))\n return;\n outroing.add(block);\n outros.c.push(() => {\n outroing.delete(block);\n if (callback) {\n if (detach)\n block.d(1);\n callback();\n }\n });\n block.o(local);\n }\n}\nconst null_transition = { duration: 0 };\nfunction create_in_transition(node, fn, params) {\n let config = fn(node, params);\n let running = false;\n let animation_name;\n let task;\n let uid = 0;\n function cleanup() {\n if (animation_name)\n delete_rule(node, animation_name);\n }\n function go() {\n const { delay = 0, duration = 300, easing = identity, tick = noop, css } = config || null_transition;\n if (css)\n animation_name = create_rule(node, 0, 1, duration, delay, easing, css, uid++);\n tick(0, 1);\n const start_time = now() + delay;\n const end_time = start_time + duration;\n if (task)\n task.abort();\n running = true;\n add_render_callback(() => dispatch(node, true, 'start'));\n task = loop(now => {\n if (running) {\n if (now >= end_time) {\n tick(1, 0);\n dispatch(node, true, 'end');\n cleanup();\n return running = false;\n }\n if (now >= start_time) {\n const t = easing((now - start_time) / duration);\n tick(t, 1 - t);\n }\n }\n return running;\n });\n }\n let started = false;\n return {\n start() {\n if (started)\n return;\n started = true;\n delete_rule(node);\n if (is_function(config)) {\n config = config();\n wait().then(go);\n }\n else {\n go();\n }\n },\n invalidate() {\n started = false;\n },\n end() {\n if (running) {\n cleanup();\n running = false;\n }\n }\n };\n}\nfunction create_out_transition(node, fn, params) {\n let config = fn(node, params);\n let running = true;\n let animation_name;\n const group = outros;\n group.r += 1;\n function go() {\n const { delay = 0, duration = 300, easing = identity, tick = noop, css } = config || null_transition;\n if (css)\n animation_name = create_rule(node, 1, 0, duration, delay, easing, css);\n const start_time = now() + delay;\n const end_time = start_time + duration;\n add_render_callback(() => dispatch(node, false, 'start'));\n loop(now => {\n if (running) {\n if (now >= end_time) {\n tick(0, 1);\n dispatch(node, false, 'end');\n if (!--group.r) {\n // this will result in `end()` being called,\n // so we don't need to clean up here\n run_all(group.c);\n }\n return false;\n }\n if (now >= start_time) {\n const t = easing((now - start_time) / duration);\n tick(1 - t, t);\n }\n }\n return running;\n });\n }\n if (is_function(config)) {\n wait().then(() => {\n // @ts-ignore\n config = config();\n go();\n });\n }\n else {\n go();\n }\n return {\n end(reset) {\n if (reset && config.tick) {\n config.tick(1, 0);\n }\n if (running) {\n if (animation_name)\n delete_rule(node, animation_name);\n running = false;\n }\n }\n };\n}\nfunction create_bidirectional_transition(node, fn, params, intro) {\n let config = fn(node, params);\n let t = intro ? 0 : 1;\n let running_program = null;\n let pending_program = null;\n let animation_name = null;\n function clear_animation() {\n if (animation_name)\n delete_rule(node, animation_name);\n }\n function init(program, duration) {\n const d = (program.b - t);\n duration *= Math.abs(d);\n return {\n a: t,\n b: program.b,\n d,\n duration,\n start: program.start,\n end: program.start + duration,\n group: program.group\n };\n }\n function go(b) {\n const { delay = 0, duration = 300, easing = identity, tick = noop, css } = config || null_transition;\n const program = {\n start: now() + delay,\n b\n };\n if (!b) {\n // @ts-ignore todo: improve typings\n program.group = outros;\n outros.r += 1;\n }\n if (running_program || pending_program) {\n pending_program = program;\n }\n else {\n // if this is an intro, and there's a delay, we need to do\n // an initial tick and/or apply CSS animation immediately\n if (css) {\n clear_animation();\n animation_name = create_rule(node, t, b, duration, delay, easing, css);\n }\n if (b)\n tick(0, 1);\n running_program = init(program, duration);\n add_render_callback(() => dispatch(node, b, 'start'));\n loop(now => {\n if (pending_program && now > pending_program.start) {\n running_program = init(pending_program, duration);\n pending_program = null;\n dispatch(node, running_program.b, 'start');\n if (css) {\n clear_animation();\n animation_name = create_rule(node, t, running_program.b, running_program.duration, 0, easing, config.css);\n }\n }\n if (running_program) {\n if (now >= running_program.end) {\n tick(t = running_program.b, 1 - t);\n dispatch(node, running_program.b, 'end');\n if (!pending_program) {\n // we're done\n if (running_program.b) {\n // intro — we can tidy up immediately\n clear_animation();\n }\n else {\n // outro — needs to be coordinated\n if (!--running_program.group.r)\n run_all(running_program.group.c);\n }\n }\n running_program = null;\n }\n else if (now >= running_program.start) {\n const p = now - running_program.start;\n t = running_program.a + running_program.d * easing(p / running_program.duration);\n tick(t, 1 - t);\n }\n }\n return !!(running_program || pending_program);\n });\n }\n }\n return {\n run(b) {\n if (is_function(config)) {\n wait().then(() => {\n // @ts-ignore\n config = config();\n go(b);\n });\n }\n else {\n go(b);\n }\n },\n end() {\n clear_animation();\n running_program = pending_program = null;\n }\n };\n}\n\nfunction handle_promise(promise, info) {\n const token = info.token = {};\n function update(type, index, key, value) {\n if (info.token !== token)\n return;\n info.resolved = value;\n let child_ctx = info.ctx;\n if (key !== undefined) {\n child_ctx = child_ctx.slice();\n child_ctx[key] = value;\n }\n const block = type && (info.current = type)(child_ctx);\n let needs_flush = false;\n if (info.block) {\n if (info.blocks) {\n info.blocks.forEach((block, i) => {\n if (i !== index && block) {\n group_outros();\n transition_out(block, 1, 1, () => {\n if (info.blocks[i] === block) {\n info.blocks[i] = null;\n }\n });\n check_outros();\n }\n });\n }\n else {\n info.block.d(1);\n }\n block.c();\n transition_in(block, 1);\n block.m(info.mount(), info.anchor);\n needs_flush = true;\n }\n info.block = block;\n if (info.blocks)\n info.blocks[index] = block;\n if (needs_flush) {\n flush();\n }\n }\n if (is_promise(promise)) {\n const current_component = get_current_component();\n promise.then(value => {\n set_current_component(current_component);\n update(info.then, 1, info.value, value);\n set_current_component(null);\n }, error => {\n set_current_component(current_component);\n update(info.catch, 2, info.error, error);\n set_current_component(null);\n if (!info.hasCatch) {\n throw error;\n }\n });\n // if we previously had a then/catch block, destroy it\n if (info.current !== info.pending) {\n update(info.pending, 0);\n return true;\n }\n }\n else {\n if (info.current !== info.then) {\n update(info.then, 1, info.value, promise);\n return true;\n }\n info.resolved = promise;\n }\n}\nfunction update_await_block_branch(info, ctx, dirty) {\n const child_ctx = ctx.slice();\n const { resolved } = info;\n if (info.current === info.then) {\n child_ctx[info.value] = resolved;\n }\n if (info.current === info.catch) {\n child_ctx[info.error] = resolved;\n }\n info.block.p(child_ctx, dirty);\n}\n\nconst globals = (typeof window !== 'undefined'\n ? window\n : typeof globalThis !== 'undefined'\n ? globalThis\n : global);\n\nfunction destroy_block(block, lookup) {\n block.d(1);\n lookup.delete(block.key);\n}\nfunction outro_and_destroy_block(block, lookup) {\n transition_out(block, 1, 1, () => {\n lookup.delete(block.key);\n });\n}\nfunction fix_and_destroy_block(block, lookup) {\n block.f();\n destroy_block(block, lookup);\n}\nfunction fix_and_outro_and_destroy_block(block, lookup) {\n block.f();\n outro_and_destroy_block(block, lookup);\n}\nfunction update_keyed_each(old_blocks, dirty, get_key, dynamic, ctx, list, lookup, node, destroy, create_each_block, next, get_context) {\n let o = old_blocks.length;\n let n = list.length;\n let i = o;\n const old_indexes = {};\n while (i--)\n old_indexes[old_blocks[i].key] = i;\n const new_blocks = [];\n const new_lookup = new Map();\n const deltas = new Map();\n i = n;\n while (i--) {\n const child_ctx = get_context(ctx, list, i);\n const key = get_key(child_ctx);\n let block = lookup.get(key);\n if (!block) {\n block = create_each_block(key, child_ctx);\n block.c();\n }\n else if (dynamic) {\n block.p(child_ctx, dirty);\n }\n new_lookup.set(key, new_blocks[i] = block);\n if (key in old_indexes)\n deltas.set(key, Math.abs(i - old_indexes[key]));\n }\n const will_move = new Set();\n const did_move = new Set();\n function insert(block) {\n transition_in(block, 1);\n block.m(node, next);\n lookup.set(block.key, block);\n next = block.first;\n n--;\n }\n while (o && n) {\n const new_block = new_blocks[n - 1];\n const old_block = old_blocks[o - 1];\n const new_key = new_block.key;\n const old_key = old_block.key;\n if (new_block === old_block) {\n // do nothing\n next = new_block.first;\n o--;\n n--;\n }\n else if (!new_lookup.has(old_key)) {\n // remove old block\n destroy(old_block, lookup);\n o--;\n }\n else if (!lookup.has(new_key) || will_move.has(new_key)) {\n insert(new_block);\n }\n else if (did_move.has(old_key)) {\n o--;\n }\n else if (deltas.get(new_key) > deltas.get(old_key)) {\n did_move.add(new_key);\n insert(new_block);\n }\n else {\n will_move.add(old_key);\n o--;\n }\n }\n while (o--) {\n const old_block = old_blocks[o];\n if (!new_lookup.has(old_block.key))\n destroy(old_block, lookup);\n }\n while (n)\n insert(new_blocks[n - 1]);\n return new_blocks;\n}\nfunction validate_each_keys(ctx, list, get_context, get_key) {\n const keys = new Set();\n for (let i = 0; i < list.length; i++) {\n const key = get_key(get_context(ctx, list, i));\n if (keys.has(key)) {\n throw new Error('Cannot have duplicate keys in a keyed each');\n }\n keys.add(key);\n }\n}\n\nfunction get_spread_update(levels, updates) {\n const update = {};\n const to_null_out = {};\n const accounted_for = { $$scope: 1 };\n let i = levels.length;\n while (i--) {\n const o = levels[i];\n const n = updates[i];\n if (n) {\n for (const key in o) {\n if (!(key in n))\n to_null_out[key] = 1;\n }\n for (const key in n) {\n if (!accounted_for[key]) {\n update[key] = n[key];\n accounted_for[key] = 1;\n }\n }\n levels[i] = n;\n }\n else {\n for (const key in o) {\n accounted_for[key] = 1;\n }\n }\n }\n for (const key in to_null_out) {\n if (!(key in update))\n update[key] = undefined;\n }\n return update;\n}\nfunction get_spread_object(spread_props) {\n return typeof spread_props === 'object' && spread_props !== null ? spread_props : {};\n}\n\n// source: https://html.spec.whatwg.org/multipage/indices.html\nconst boolean_attributes = new Set([\n 'allowfullscreen',\n 'allowpaymentrequest',\n 'async',\n 'autofocus',\n 'autoplay',\n 'checked',\n 'controls',\n 'default',\n 'defer',\n 'disabled',\n 'formnovalidate',\n 'hidden',\n 'ismap',\n 'loop',\n 'multiple',\n 'muted',\n 'nomodule',\n 'novalidate',\n 'open',\n 'playsinline',\n 'readonly',\n 'required',\n 'reversed',\n 'selected'\n]);\n\nconst invalid_attribute_name_character = /[\\s'\">/=\\u{FDD0}-\\u{FDEF}\\u{FFFE}\\u{FFFF}\\u{1FFFE}\\u{1FFFF}\\u{2FFFE}\\u{2FFFF}\\u{3FFFE}\\u{3FFFF}\\u{4FFFE}\\u{4FFFF}\\u{5FFFE}\\u{5FFFF}\\u{6FFFE}\\u{6FFFF}\\u{7FFFE}\\u{7FFFF}\\u{8FFFE}\\u{8FFFF}\\u{9FFFE}\\u{9FFFF}\\u{AFFFE}\\u{AFFFF}\\u{BFFFE}\\u{BFFFF}\\u{CFFFE}\\u{CFFFF}\\u{DFFFE}\\u{DFFFF}\\u{EFFFE}\\u{EFFFF}\\u{FFFFE}\\u{FFFFF}\\u{10FFFE}\\u{10FFFF}]/u;\n// https://html.spec.whatwg.org/multipage/syntax.html#attributes-2\n// https://infra.spec.whatwg.org/#noncharacter\nfunction spread(args, classes_to_add) {\n const attributes = Object.assign({}, ...args);\n if (classes_to_add) {\n if (attributes.class == null) {\n attributes.class = classes_to_add;\n }\n else {\n attributes.class += ' ' + classes_to_add;\n }\n }\n let str = '';\n Object.keys(attributes).forEach(name => {\n if (invalid_attribute_name_character.test(name))\n return;\n const value = attributes[name];\n if (value === true)\n str += ' ' + name;\n else if (boolean_attributes.has(name.toLowerCase())) {\n if (value)\n str += ' ' + name;\n }\n else if (value != null) {\n str += ` ${name}=\"${value}\"`;\n }\n });\n return str;\n}\nconst escaped = {\n '\"': '"',\n \"'\": ''',\n '&': '&',\n '<': '<',\n '>': '>'\n};\nfunction escape(html) {\n return String(html).replace(/[\"'&<>]/g, match => escaped[match]);\n}\nfunction escape_attribute_value(value) {\n return typeof value === 'string' ? escape(value) : value;\n}\nfunction escape_object(obj) {\n const result = {};\n for (const key in obj) {\n result[key] = escape_attribute_value(obj[key]);\n }\n return result;\n}\nfunction each(items, fn) {\n let str = '';\n for (let i = 0; i < items.length; i += 1) {\n str += fn(items[i], i);\n }\n return str;\n}\nconst missing_component = {\n $$render: () => ''\n};\nfunction validate_component(component, name) {\n if (!component || !component.$$render) {\n if (name === 'svelte:component')\n name += ' this={...}';\n throw new Error(`<${name}> is not a valid SSR component. You may need to review your build config to ensure that dependencies are compiled, rather than imported as pre-compiled modules`);\n }\n return component;\n}\nfunction debug(file, line, column, values) {\n console.log(`{@debug} ${file ? file + ' ' : ''}(${line}:${column})`); // eslint-disable-line no-console\n console.log(values); // eslint-disable-line no-console\n return '';\n}\nlet on_destroy;\nfunction create_ssr_component(fn) {\n function $$render(result, props, bindings, slots, context) {\n const parent_component = current_component;\n const $$ = {\n on_destroy,\n context: new Map(parent_component ? parent_component.$$.context : context || []),\n // these will be immediately discarded\n on_mount: [],\n before_update: [],\n after_update: [],\n callbacks: blank_object()\n };\n set_current_component({ $$ });\n const html = fn(result, props, bindings, slots);\n set_current_component(parent_component);\n return html;\n }\n return {\n render: (props = {}, { $$slots = {}, context = new Map() } = {}) => {\n on_destroy = [];\n const result = { title: '', head: '', css: new Set() };\n const html = $$render(result, props, {}, $$slots, context);\n run_all(on_destroy);\n return {\n html,\n css: {\n code: Array.from(result.css).map(css => css.code).join('\\n'),\n map: null // TODO\n },\n head: result.title + result.head\n };\n },\n $$render\n };\n}\nfunction add_attribute(name, value, boolean) {\n if (value == null || (boolean && !value))\n return '';\n return ` ${name}${value === true ? '' : `=${typeof value === 'string' ? JSON.stringify(escape(value)) : `\"${value}\"`}`}`;\n}\nfunction add_classes(classes) {\n return classes ? ` class=\"${classes}\"` : '';\n}\n\nfunction bind(component, name, callback) {\n const index = component.$$.props[name];\n if (index !== undefined) {\n component.$$.bound[index] = callback;\n callback(component.$$.ctx[index]);\n }\n}\nfunction create_component(block) {\n block && block.c();\n}\nfunction claim_component(block, parent_nodes) {\n block && block.l(parent_nodes);\n}\nfunction mount_component(component, target, anchor, customElement) {\n const { fragment, on_mount, on_destroy, after_update } = component.$$;\n fragment && fragment.m(target, anchor);\n if (!customElement) {\n // onMount happens before the initial afterUpdate\n add_render_callback(() => {\n const new_on_destroy = on_mount.map(run).filter(is_function);\n if (on_destroy) {\n on_destroy.push(...new_on_destroy);\n }\n else {\n // Edge case - component was destroyed immediately,\n // most likely as a result of a binding initialising\n run_all(new_on_destroy);\n }\n component.$$.on_mount = [];\n });\n }\n after_update.forEach(add_render_callback);\n}\nfunction destroy_component(component, detaching) {\n const $$ = component.$$;\n if ($$.fragment !== null) {\n run_all($$.on_destroy);\n $$.fragment && $$.fragment.d(detaching);\n // TODO null out other refs, including component.$$ (but need to\n // preserve final state?)\n $$.on_destroy = $$.fragment = null;\n $$.ctx = [];\n }\n}\nfunction make_dirty(component, i) {\n if (component.$$.dirty[0] === -1) {\n dirty_components.push(component);\n schedule_update();\n component.$$.dirty.fill(0);\n }\n component.$$.dirty[(i / 31) | 0] |= (1 << (i % 31));\n}\nfunction init(component, options, instance, create_fragment, not_equal, props, append_styles, dirty = [-1]) {\n const parent_component = current_component;\n set_current_component(component);\n const $$ = component.$$ = {\n fragment: null,\n ctx: null,\n // state\n props,\n update: noop,\n not_equal,\n bound: blank_object(),\n // lifecycle\n on_mount: [],\n on_destroy: [],\n on_disconnect: [],\n before_update: [],\n after_update: [],\n context: new Map(parent_component ? parent_component.$$.context : options.context || []),\n // everything else\n callbacks: blank_object(),\n dirty,\n skip_bound: false,\n root: options.target || parent_component.$$.root\n };\n append_styles && append_styles($$.root);\n let ready = false;\n $$.ctx = instance\n ? instance(component, options.props || {}, (i, ret, ...rest) => {\n const value = rest.length ? rest[0] : ret;\n if ($$.ctx && not_equal($$.ctx[i], $$.ctx[i] = value)) {\n if (!$$.skip_bound && $$.bound[i])\n $$.bound[i](value);\n if (ready)\n make_dirty(component, i);\n }\n return ret;\n })\n : [];\n $$.update();\n ready = true;\n run_all($$.before_update);\n // `false` as a special case of no DOM component\n $$.fragment = create_fragment ? create_fragment($$.ctx) : false;\n if (options.target) {\n if (options.hydrate) {\n start_hydrating();\n const nodes = children(options.target);\n // eslint-disable-next-line @typescript-eslint/no-non-null-assertion\n $$.fragment && $$.fragment.l(nodes);\n nodes.forEach(detach);\n }\n else {\n // eslint-disable-next-line @typescript-eslint/no-non-null-assertion\n $$.fragment && $$.fragment.c();\n }\n if (options.intro)\n transition_in(component.$$.fragment);\n mount_component(component, options.target, options.anchor, options.customElement);\n end_hydrating();\n flush();\n }\n set_current_component(parent_component);\n}\nlet SvelteElement;\nif (typeof HTMLElement === 'function') {\n SvelteElement = class extends HTMLElement {\n constructor() {\n super();\n this.attachShadow({ mode: 'open' });\n }\n connectedCallback() {\n const { on_mount } = this.$$;\n this.$$.on_disconnect = on_mount.map(run).filter(is_function);\n // @ts-ignore todo: improve typings\n for (const key in this.$$.slotted) {\n // @ts-ignore todo: improve typings\n this.appendChild(this.$$.slotted[key]);\n }\n }\n attributeChangedCallback(attr, _oldValue, newValue) {\n this[attr] = newValue;\n }\n disconnectedCallback() {\n run_all(this.$$.on_disconnect);\n }\n $destroy() {\n destroy_component(this, 1);\n this.$destroy = noop;\n }\n $on(type, callback) {\n // TODO should this delegate to addEventListener?\n const callbacks = (this.$$.callbacks[type] || (this.$$.callbacks[type] = []));\n callbacks.push(callback);\n return () => {\n const index = callbacks.indexOf(callback);\n if (index !== -1)\n callbacks.splice(index, 1);\n };\n }\n $set($$props) {\n if (this.$$set && !is_empty($$props)) {\n this.$$.skip_bound = true;\n this.$$set($$props);\n this.$$.skip_bound = false;\n }\n }\n };\n}\n/**\n * Base class for Svelte components. Used when dev=false.\n */\nclass SvelteComponent {\n $destroy() {\n destroy_component(this, 1);\n this.$destroy = noop;\n }\n $on(type, callback) {\n const callbacks = (this.$$.callbacks[type] || (this.$$.callbacks[type] = []));\n callbacks.push(callback);\n return () => {\n const index = callbacks.indexOf(callback);\n if (index !== -1)\n callbacks.splice(index, 1);\n };\n }\n $set($$props) {\n if (this.$$set && !is_empty($$props)) {\n this.$$.skip_bound = true;\n this.$$set($$props);\n this.$$.skip_bound = false;\n }\n }\n}\n\nfunction dispatch_dev(type, detail) {\n document.dispatchEvent(custom_event(type, Object.assign({ version: '3.42.1' }, detail), true));\n}\nfunction append_dev(target, node) {\n dispatch_dev('SvelteDOMInsert', { target, node });\n append(target, node);\n}\nfunction append_hydration_dev(target, node) {\n dispatch_dev('SvelteDOMInsert', { target, node });\n append_hydration(target, node);\n}\nfunction insert_dev(target, node, anchor) {\n dispatch_dev('SvelteDOMInsert', { target, node, anchor });\n insert(target, node, anchor);\n}\nfunction insert_hydration_dev(target, node, anchor) {\n dispatch_dev('SvelteDOMInsert', { target, node, anchor });\n insert_hydration(target, node, anchor);\n}\nfunction detach_dev(node) {\n dispatch_dev('SvelteDOMRemove', { node });\n detach(node);\n}\nfunction detach_between_dev(before, after) {\n while (before.nextSibling && before.nextSibling !== after) {\n detach_dev(before.nextSibling);\n }\n}\nfunction detach_before_dev(after) {\n while (after.previousSibling) {\n detach_dev(after.previousSibling);\n }\n}\nfunction detach_after_dev(before) {\n while (before.nextSibling) {\n detach_dev(before.nextSibling);\n }\n}\nfunction listen_dev(node, event, handler, options, has_prevent_default, has_stop_propagation) {\n const modifiers = options === true ? ['capture'] : options ? Array.from(Object.keys(options)) : [];\n if (has_prevent_default)\n modifiers.push('preventDefault');\n if (has_stop_propagation)\n modifiers.push('stopPropagation');\n dispatch_dev('SvelteDOMAddEventListener', { node, event, handler, modifiers });\n const dispose = listen(node, event, handler, options);\n return () => {\n dispatch_dev('SvelteDOMRemoveEventListener', { node, event, handler, modifiers });\n dispose();\n };\n}\nfunction attr_dev(node, attribute, value) {\n attr(node, attribute, value);\n if (value == null)\n dispatch_dev('SvelteDOMRemoveAttribute', { node, attribute });\n else\n dispatch_dev('SvelteDOMSetAttribute', { node, attribute, value });\n}\nfunction prop_dev(node, property, value) {\n node[property] = value;\n dispatch_dev('SvelteDOMSetProperty', { node, property, value });\n}\nfunction dataset_dev(node, property, value) {\n node.dataset[property] = value;\n dispatch_dev('SvelteDOMSetDataset', { node, property, value });\n}\nfunction set_data_dev(text, data) {\n data = '' + data;\n if (text.wholeText === data)\n return;\n dispatch_dev('SvelteDOMSetData', { node: text, data });\n text.data = data;\n}\nfunction validate_each_argument(arg) {\n if (typeof arg !== 'string' && !(arg && typeof arg === 'object' && 'length' in arg)) {\n let msg = '{#each} only iterates over array-like objects.';\n if (typeof Symbol === 'function' && arg && Symbol.iterator in arg) {\n msg += ' You can use a spread to convert this iterable into an array.';\n }\n throw new Error(msg);\n }\n}\nfunction validate_slots(name, slot, keys) {\n for (const slot_key of Object.keys(slot)) {\n if (!~keys.indexOf(slot_key)) {\n console.warn(`<${name}> received an unexpected slot \"${slot_key}\".`);\n }\n }\n}\n/**\n * Base class for Svelte components with some minor dev-enhancements. Used when dev=true.\n */\nclass SvelteComponentDev extends SvelteComponent {\n constructor(options) {\n if (!options || (!options.target && !options.$$inline)) {\n throw new Error(\"'target' is a required option\");\n }\n super();\n }\n $destroy() {\n super.$destroy();\n this.$destroy = () => {\n console.warn('Component was already destroyed'); // eslint-disable-line no-console\n };\n }\n $capture_state() { }\n $inject_state() { }\n}\n/**\n * Base class to create strongly typed Svelte components.\n * This only exists for typing purposes and should be used in `.d.ts` files.\n *\n * ### Example:\n *\n * You have component library on npm called `component-library`, from which\n * you export a component called `MyComponent`. For Svelte+TypeScript users,\n * you want to provide typings. Therefore you create a `index.d.ts`:\n * ```ts\n * import { SvelteComponentTyped } from \"svelte\";\n * export class MyComponent extends SvelteComponentTyped<{foo: string}> {}\n * ```\n * Typing this makes it possible for IDEs like VS Code with the Svelte extension\n * to provide intellisense and to use the component like this in a Svelte file\n * with TypeScript:\n * ```svelte\n * \n * \n * ```\n *\n * #### Why not make this part of `SvelteComponent(Dev)`?\n * Because\n * ```ts\n * class ASubclassOfSvelteComponent extends SvelteComponent<{foo: string}> {}\n * const component: typeof SvelteComponent = ASubclassOfSvelteComponent;\n * ```\n * will throw a type error, so we need to separate the more strictly typed class.\n */\nclass SvelteComponentTyped extends SvelteComponentDev {\n constructor(options) {\n super(options);\n }\n}\nfunction loop_guard(timeout) {\n const start = Date.now();\n return () => {\n if (Date.now() - start > timeout) {\n throw new Error('Infinite loop detected');\n }\n };\n}\n\nexport { HtmlTag, HtmlTagHydration, SvelteComponent, SvelteComponentDev, SvelteComponentTyped, SvelteElement, action_destroyer, add_attribute, add_classes, add_flush_callback, add_location, add_render_callback, add_resize_listener, add_transform, afterUpdate, append, append_dev, append_empty_stylesheet, append_hydration, append_hydration_dev, append_styles, assign, attr, attr_dev, attribute_to_object, beforeUpdate, bind, binding_callbacks, blank_object, bubble, check_outros, children, claim_component, claim_element, claim_html_tag, claim_space, claim_text, clear_loops, component_subscribe, compute_rest_props, compute_slots, createEventDispatcher, create_animation, create_bidirectional_transition, create_component, create_in_transition, create_out_transition, create_slot, create_ssr_component, current_component, custom_event, dataset_dev, debug, destroy_block, destroy_component, destroy_each, detach, detach_after_dev, detach_before_dev, detach_between_dev, detach_dev, dirty_components, dispatch_dev, each, element, element_is, empty, end_hydrating, escape, escape_attribute_value, escape_object, escaped, exclude_internal_props, fix_and_destroy_block, fix_and_outro_and_destroy_block, fix_position, flush, getAllContexts, getContext, get_all_dirty_from_scope, get_binding_group_value, get_current_component, get_custom_elements_slots, get_root_for_style, get_slot_changes, get_spread_object, get_spread_update, get_store_value, globals, group_outros, handle_promise, hasContext, has_prop, identity, init, insert, insert_dev, insert_hydration, insert_hydration_dev, intros, invalid_attribute_name_character, is_client, is_crossorigin, is_empty, is_function, is_promise, listen, listen_dev, loop, loop_guard, missing_component, mount_component, noop, not_equal, now, null_to_empty, object_without_properties, onDestroy, onMount, once, outro_and_destroy_block, prevent_default, prop_dev, query_selector_all, raf, run, run_all, safe_not_equal, schedule_update, select_multiple_value, select_option, select_options, select_value, self, setContext, set_attributes, set_current_component, set_custom_element_data, set_data, set_data_dev, set_input_type, set_input_value, set_now, set_raf, set_store_value, set_style, set_svg_attributes, space, spread, src_url_equal, start_hydrating, stop_propagation, subscribe, svg_element, text, tick, time_ranges_to_array, to_number, toggle_class, transition_in, transition_out, trusted, update_await_block_branch, update_keyed_each, update_slot, update_slot_base, validate_component, validate_each_argument, validate_each_keys, validate_slots, validate_store, xlink_attr };\n","\n\n\n\n\n\n\n\n\n\n\n
\n\t\n\t\n\t\n
\n\n","import App from './App.svelte';\n\nvar app = new App({\n\ttarget: document.body\n});\n\nexport default app;"],"names":[],"mappings":"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"} \ No newline at end of file diff --git a/frontend/public/favicon.png b/frontend/public/favicon.png new file mode 100644 index 0000000000000000000000000000000000000000..7e6f5eb5a2f1f1c882d265cf479de25caa925645 GIT binary patch literal 3127 zcmV-749N3|P)i z7)}s4L53SJCkR}iVi00SFk;`MXX*#X*kkwKs@nFGS}c;=?XFjU|G$3t^5sjIVS2G+ zw)WGF83CpoGXhLGW(1gW%uV|X7>1P6VhCX=Ux)Lb!*DZ%@I3!{Gsf7d?gtIQ%nQiK z3%(LUSkBji;C5Rfgd6$VsF@H`Pk@xtY6t<>FNR-pD}=C~$?)9pdm3XZ36N5PNWYjb z$xd$yNQR9N!dfj-Vd@BwQo^FIIWPPmT&sZyQ$v81(sCBV=PGy{0wltEjB%~h157*t zvbe_!{=I_783x!0t1-r#-d{Y?ae$Q4N_Nd^Ui^@y(%)Gjou6y<3^XJdu{rmUf-Me?)zZ>9OR&6U5H*cK; z$gUlB{g0O4gN0sLSO|Of?hU(l?;h(jA3uH!Z{EBKuV23ouU@^Y6#%v+QG;>e*E}%?wlu-NT4DG zs)z)7WbLr)vGAu(ohrKc^em@OpO&f~6_>E61n_e0_V3@{U3^O;j{`^mNCJUj_>;7v zsMs6Hu3g7+@v+lSo;=yTYFqq}jZmQ-BK8K{C4kqi_i*jBaQE(Au0607V-zKeT;EPg zX(`vrn=L+e74+-Tqeok@_`tDa$G9I|$nTU5H*2V8@y()n*zqM?J1G!-1aX;CfDC9B zTnJ#j_%*n8Qb1)re*Bno7g0RG{Eb;IK14irJYJp$5Z6ac9~b_P?+5t~95~SRG$g?1 znFJ7p$xV&GZ18m~79TGRdfsc-BcX$9yXTR*n)mPD@1~O(_?cT$ZvFPucRmGlq&se0 zKrcUf^k}4hM*biEJOWKzz!qQe;CB_ZtSOO9Owg#lZAc=s65^rb{fZe(TYu_rk!wKkEf}RIt=#Om( zR8mN`DM<^xj~59euMMspBolVN zAPTr8sSDI104orIAdmL$uOXn*6hga1G+0WD0E?UtabxC#VC~vf3|10|phW;yQ3CY8 z2CM=)ErF;xq-YJ5G|um}>*1#E+O_Mu|Nr#qQ&G1P-NMq@f?@*XUcSbV?tX=)ilM-Q zBZP|!Bpv0V;#ojKcpc7$=eqO;#Uy~#?^kNI{vSZfLx&DEt~LTmaKWXcx=joubklI<*Aw z>LtMaQ7DR<1I2LkWvwyu#Rwn~;ezT}_g(@5l3h?W%-a86Y-t#O1PubP+z<%?V5D(U zy57A6{h+{?kOZp7&WKZR+=sznMJ}+Dnpo=C_0%R_x_t~J5T?E_{+))l5v1%52>)d-`iiZyx|5!%M2Fb2dU zW3~MwwpEH9Rhue+k$UIOoo($Ds!NbOyMR36fRHu;*15(YcA7siIZk#%JWz>P!qX1?IUojG&nKR>^gArBt2 zit(ETyZ=@V&7mv_Fi4bABcnwP+jzQuHcfU&BrAV91u-rFvEi7y-KnWsvHH=d2 zgAk(GKm_S8RcTJ>2N3~&Hbwp{Z3NF_Xeh}g4Eke)V&dY{W(3&b1j9t4yK_aYJisZZ{1rcU5- z;eD>K;ndPq&B-8yA_S0F!4ThA&{1{x)H<#?k9a#6Pc6L?V^s0``ynL&D;p(!Nmx`Y zFkHex{4p!Ggm^@DlehW}iHHVi}~u=$&N? z(NEBLQ#UxxAkdW>X9LnqUr#t4Lu0=9L8&o>JsqTtT5|%gb3QA~hr0pED71+iFFr)dZ=Q=E6ng{NE{Z~0)C?deO#?Aj zSDQ$z#TeC2T^|=}6GBo-&$;E{HL3!q3Z-szuf)O=G#zDjin4SSP%o%6+2IT#sLjQa ziyxFFz~LMjWY+_a5H!U6%a<=b7QVP^ z*90a62;bVq{?@)P6^DWd^Yilq4|YTV2Nw!Yu;a1lPI-sxR)rf@Fe5DhDP7FH zZZ%4S*1C30P;|O+jB!1;m|rXT90Sm5*RBbQN`PKu+hDD*S^yE(CdtSfg=z>u$cIj> z - - - - - - - - - - - React App - - - -
- - + + + + + diff --git a/frontend/rollup.config.js b/frontend/rollup.config.js new file mode 100644 index 0000000..e8965ec --- /dev/null +++ b/frontend/rollup.config.js @@ -0,0 +1,76 @@ +import svelte from 'rollup-plugin-svelte'; +import commonjs from '@rollup/plugin-commonjs'; +import resolve from '@rollup/plugin-node-resolve'; +import livereload from 'rollup-plugin-livereload'; +import { terser } from 'rollup-plugin-terser'; +import css from 'rollup-plugin-css-only'; + +const production = !process.env.ROLLUP_WATCH; + +function serve() { + let server; + + function toExit() { + if (server) server.kill(0); + } + + return { + writeBundle() { + if (server) return; + server = require('child_process').spawn('npm', ['run', 'start', '--', '--dev'], { + stdio: ['ignore', 'inherit', 'inherit'], + shell: true + }); + + process.on('SIGTERM', toExit); + process.on('exit', toExit); + } + }; +} + +export default { + input: 'src/main.js', + output: { + sourcemap: true, + format: 'iife', + name: 'app', + file: 'public/build/bundle.js' + }, + plugins: [ + svelte({ + compilerOptions: { + // enable run-time checks when not in production + dev: !production + } + }), + // we'll extract any component CSS out into + // a separate file - better for performance + css({ output: 'bundle.css' }), + + // If you have external dependencies installed from + // npm, you'll most likely need these plugins. In + // some cases you'll need additional configuration - + // consult the documentation for details: + // https://github.com/rollup/plugins/tree/master/packages/commonjs + resolve({ + browser: true, + dedupe: ['svelte'] + }), + commonjs(), + + // In dev mode, call `npm run start` once + // the bundle has been generated + !production && serve(), + + // Watch the `public` directory and refresh the + // browser on changes when not in production + !production && livereload('public'), + + // If we're building for production (npm run build + // instead of npm run dev), minify + production && terser() + ], + watch: { + clearScreen: false + } +}; diff --git a/frontend/scripts/setupTypeScript.js b/frontend/scripts/setupTypeScript.js new file mode 100644 index 0000000..133658a --- /dev/null +++ b/frontend/scripts/setupTypeScript.js @@ -0,0 +1,121 @@ +// @ts-check + +/** This script modifies the project to support TS code in .svelte files like: + + + + As well as validating the code for CI. + */ + +/** To work on this script: + rm -rf test-template template && git clone sveltejs/template test-template && node scripts/setupTypeScript.js test-template +*/ + +const fs = require("fs") +const path = require("path") +const { argv } = require("process") + +const projectRoot = argv[2] || path.join(__dirname, "..") + +// Add deps to pkg.json +const packageJSON = JSON.parse(fs.readFileSync(path.join(projectRoot, "package.json"), "utf8")) +packageJSON.devDependencies = Object.assign(packageJSON.devDependencies, { + "svelte-check": "^2.0.0", + "svelte-preprocess": "^4.0.0", + "@rollup/plugin-typescript": "^8.0.0", + "typescript": "^4.0.0", + "tslib": "^2.0.0", + "@tsconfig/svelte": "^2.0.0" +}) + +// Add script for checking +packageJSON.scripts = Object.assign(packageJSON.scripts, { + "check": "svelte-check --tsconfig ./tsconfig.json" +}) + +// Write the package JSON +fs.writeFileSync(path.join(projectRoot, "package.json"), JSON.stringify(packageJSON, null, " ")) + +// mv src/main.js to main.ts - note, we need to edit rollup.config.js for this too +const beforeMainJSPath = path.join(projectRoot, "src", "main.js") +const afterMainTSPath = path.join(projectRoot, "src", "main.ts") +fs.renameSync(beforeMainJSPath, afterMainTSPath) + +// Switch the app.svelte file to use TS +const appSveltePath = path.join(projectRoot, "src", "App.svelte") +let appFile = fs.readFileSync(appSveltePath, "utf8") +appFile = appFile.replace(" + + + + + + + + +
+ + + +
+ + \ No newline at end of file diff --git a/frontend/src/Navbar.js b/frontend/src/Navbar.js deleted file mode 100644 index 109af8f..0000000 --- a/frontend/src/Navbar.js +++ /dev/null @@ -1,122 +0,0 @@ -import Grid from '@material-ui/core/Grid'; -import GroupIcon from '@material-ui/icons/Group'; -import SettingsIcon from '@material-ui/icons/Settings'; -import AssignmentIndIcon from '@material-ui/icons/AssignmentInd'; -import Box from '@material-ui/core/Box'; -import Typography from '@material-ui/core/Typography'; -import Button from '@material-ui/core/Button'; -import ExitToAppIcon from '@material-ui/icons/ExitToApp'; -import { connect } from 'react-redux'; -import { withStyles } from '@material-ui/styles'; -import { logout } from './reducers/login'; - -const styles = (theme) => { - return { - container: { - width: '100%' - }, - flexbox: { - display: 'flex', - flexGrow: 1, - width: 'auto' - }, - buttonWrapper: { - alignItems: 'flex-end' - }, - button: { - alignItems: 'flex-end' - }, - typography: { - margin: '5px 0px' - } - }; -}; - -const Navbar = (props) => { - const { classes } = props; - return ( -
- - - - - - - - -
- ); -}; - -const LoginLogoutButton = connect( - (state) => { - return { - token: state.login.token - }; - }, - (dispatch, props) => { - return {}; - } -)(({ children, ...props }) => { - const { classes } = props; - return props.token !== undefined ? ( - <> - - - - ) : ( - <> - - - - ); -}); - -export default connect( - (state, props) => { - return {}; - }, - (dispatch, props) => { - return { - logout: () => { - dispatch(logout()); - } - }; - } -)(withStyles(styles)(Navbar)); diff --git a/frontend/src/ThemeProvider.js b/frontend/src/ThemeProvider.js deleted file mode 100644 index 75a0fa7..0000000 --- a/frontend/src/ThemeProvider.js +++ /dev/null @@ -1,37 +0,0 @@ -import CssBaseline from '@material-ui/core/CssBaseline'; -import { withStyles, withTheme } from '@material-ui/styles'; -import { createTheme, ThemeProvider } from '@material-ui/core/styles'; -import theme from './theme'; - -const styles = () => { - return { - root: { - height: '100vh', - zIndex: 1, - overflow: 'hidden', - position: 'relative', - display: 'flex' - }, - content: { - zIndex: 3, - flexGrow: 1 - }, - spacing: (n) => { - return `${n * 2}px`; - } - }; -}; - -const ThemeWrapper = (props) => { - const { children, classes } = props; - return ( - - -
- {children} -
-
- ); -}; - -export default withTheme(withStyles(styles)(ThemeWrapper)); diff --git a/frontend/src/index.js b/frontend/src/index.js deleted file mode 100644 index 7362884..0000000 --- a/frontend/src/index.js +++ /dev/null @@ -1,79 +0,0 @@ -import React from 'react'; -import ReactDOM from 'react-dom'; -import { Provider } from 'react-redux'; -import App from './App'; -import reportWebVitals from './reportWebVitals'; -import { configureStore } from '@reduxjs/toolkit'; -import RootReducer from './reducers'; -import axios from 'axios'; -import logger from 'redux-logger'; - -const defaultConfig = { - apiUrl: 'http://localhost:8080/api' -}; - -const renderApp = ({ config, user }) => { - const isDev = process.env.NODE_ENV !== 'production'; - const store = configureStore({ - devTools: isDev, - preloadedState: { - config: config, - login: user - }, - reducer: RootReducer, - middleware: (getDefaultMiddleware) => getDefaultMiddleware().concat(logger) - }); - ReactDOM.render( - - - , - document.getElementById('root') - ); -}; - -// If you want to start measuring performance in your app, pass a function -// to log results (for example: reportWebVitals(console.log)) -// or send to an analytics endpoint. Learn more: https://bit.ly/CRA-vitals -reportWebVitals(); - -const findConfig = (fullConfig) => { - return Object.assign(fullConfig.defaultConfig, fullConfig.configs[fullConfig.hosts[window.location.host]]); -}; - -axios - .get('/config.json') - .then( - (success) => { - return Object.assign(defaultConfig, findConfig(success.data)); - }, - () => { - return defaultConfig; - } - ) - .then((config) => { - const details = JSON.parse(localStorage.getItem('userDetails') || '{}'); - return axios - .get(`${config.apiUrl}/user/authorized`, { - headers: { - id: details.id, - Authorization: details.token - } - }) - .then( - (success) => { - return { - config, - user: details || {} - }; - }, - () => { - return { - config, - user: {} - }; - } - ); - }) - .then(({ config, user }) => { - renderApp({ config, user }); - }); diff --git a/frontend/src/main.js b/frontend/src/main.js new file mode 100644 index 0000000..284bd8f --- /dev/null +++ b/frontend/src/main.js @@ -0,0 +1,7 @@ +import App from './App.svelte'; + +var app = new App({ + target: document.body +}); + +export default app; \ No newline at end of file diff --git a/frontend/src/modules/About/index.js b/frontend/src/modules/About/index.js deleted file mode 100644 index 5a88a04..0000000 --- a/frontend/src/modules/About/index.js +++ /dev/null @@ -1,5 +0,0 @@ -const AboutPage = (props) => { - return
; -}; - -export default AboutPage; diff --git a/frontend/src/modules/Account/index.js b/frontend/src/modules/Account/index.js deleted file mode 100644 index dfbbd52..0000000 --- a/frontend/src/modules/Account/index.js +++ /dev/null @@ -1,5 +0,0 @@ -const AccountPage = (props) => { - return
; -}; - -export default AccountPage; diff --git a/frontend/src/modules/Login/index.js b/frontend/src/modules/Login/index.js deleted file mode 100644 index 182f070..0000000 --- a/frontend/src/modules/Login/index.js +++ /dev/null @@ -1,174 +0,0 @@ -import { login, forgotPassword } from '../../reducers/login'; -import { useState } from 'react'; -import Button from '@material-ui/core/Button'; -import Grid from '@material-ui/core/Grid'; -import InputAdornment from '@material-ui/core/InputAdornment'; -import IconButton from '@material-ui/core/IconButton'; -import TextField from '@material-ui/core/TextField'; -import Typography from '@material-ui/core/Typography'; -import Visibility from '@material-ui/icons/Visibility'; -import VisibilityOff from '@material-ui/icons/VisibilityOff'; -import { connect } from 'react-redux'; -import { withStyles } from '@material-ui/styles'; - -const styles = (theme) => { }; - -const LoginPage = (props) => { - const [email, setEmail] = useState(''); - const [password, setPassword] = useState(''); - const [visible, setVisible] = useState(false); - const [error, setError] = useState(false); - const [forgotPassword, setForgotPassword] = useState(false); - - const handleForgotPassword = () => { - if (!!email) { - props.forgotPassword(email); - setForgotPassword(false); - setError(false); - setEmail(''); - } else { - setError(true); - } - }; - return ( - - -
-
- - {forgotPassword ? 'Reset Password' : 'Sign in'} - - { - return setEmail(event.target.value ?? ''); - }} - onKeyPress={(event) => { - if (event.key === 'Enter') { - forgotPassword ? props.forgotPassword(email) : props.login(email, password); - } - }} - /> - {forgotPassword === false ? ( - <> -
- { - setPassword(event.target.value ?? ''); - }} - onKeyPress={(event) => { - if (event.key === 'Enter') { - props.login(email, password); - } - }} - InputProps={{ - endAdornment: ( - - { - return setVisible(!visible); - }} - edge='end'> - {visible ? : } - - - ) - }} - /> -
- -
-
- - - ) : ( - - - - - - - - - - - - )} -
-
-
-
- ); -}; - -export default connect( - (state) => { - return {}; - }, - (dispatch, props) => { - return { - login: (email, password) => { - dispatch(login(email, password)); - }, - forgotPassword: (email) => { - dispatch(forgotPassword(email)); - } - }; - } -)(withStyles(styles)(LoginPage)); diff --git a/frontend/src/modules/Oauth/index.js b/frontend/src/modules/Oauth/index.js deleted file mode 100644 index bf2ac69..0000000 --- a/frontend/src/modules/Oauth/index.js +++ /dev/null @@ -1,17 +0,0 @@ -import { connect } from 'react-redux'; -import { withStyles } from '@material-ui/styles'; - -const styles = (theme) => { }; - -const OauthPage = (props) => { - return
test
-} - -export default connect( - (state) => { - return {}; - }, - (dispatch, props) => { - return {}; - } -)(withStyles(styles)(OauthPage)); diff --git a/frontend/src/modules/Root/index.js b/frontend/src/modules/Root/index.js deleted file mode 100644 index d3df59d..0000000 --- a/frontend/src/modules/Root/index.js +++ /dev/null @@ -1,5 +0,0 @@ -const RootPage = (props) => { - return
; -}; - -export default RootPage; diff --git a/frontend/src/modules/Signup/index.js b/frontend/src/modules/Signup/index.js deleted file mode 100644 index 84247f7..0000000 --- a/frontend/src/modules/Signup/index.js +++ /dev/null @@ -1,162 +0,0 @@ -import { signup } from '../../reducers/login'; -import { useState } from 'react'; -import Button from '@material-ui/core/Button'; -import Grid from '@material-ui/core/Grid'; -import InputAdornment from '@material-ui/core/InputAdornment'; -import IconButton from '@material-ui/core/IconButton'; -import TextField from '@material-ui/core/TextField'; -import Typography from '@material-ui/core/Typography'; -import Visibility from '@material-ui/icons/Visibility'; -import VisibilityOff from '@material-ui/icons/VisibilityOff'; -import { connect } from 'react-redux'; -import { withStyles } from '@material-ui/styles'; - -const styles = (theme) => {}; - -const SignupPage = (props) => { - //const { classes } = props; - const [email, setEmail] = useState(''); - const [password, setPassword] = useState(''); - const [confirmPassword, setConfirmPassword] = useState(''); - const [visible, setVisible] = useState(false); - const [error, setError] = useState(false); - const checkSignup = () => { - if (password !== confirmPassword) { - setError(true); - } else { - setError(false); - } - }; - - return ( - - -
-
- - Sign Up - - { - return setEmail(event.target.value ?? ''); - }} - onKeyPress={(event) => { - if (event.key === 'Enter') { - checkSignup(); - if (!error) { - props.signup(email, password); - } - } - }} - /> -
- { - setPassword(event.target.value ?? ''); - }} - onKeyPress={(event) => { - if (event.key === 'Enter') { - checkSignup(); - if (!error) { - props.signup(email, password); - } - } - }} - InputProps={{ - endAdornment: ( - - { - return setVisible(!visible); - }} - edge='end'> - {visible ? : } - - - ) - }} - /> -
-
- { - setConfirmPassword(event.target.value ?? ''); - }} - onKeyPress={(event) => { - checkSignup(); - if (event.key === 'Enter') { - if (!error) { - props.signup(email, password); - } - } - }} - InputProps={{ - endAdornment: ( - - { - return setVisible(!visible); - }} - edge='end'> - {visible ? : } - - - ) - }} - /> -
- -
-
-
-
- ); -}; - -export default connect( - (state) => { - return {}; - }, - (dispatch, props) => { - return { - signup: (email, password) => { - dispatch(signup(email, password)); - } - }; - } -)(withStyles(styles)(SignupPage)); diff --git a/frontend/src/modules/TodoList/index.js b/frontend/src/modules/TodoList/index.js deleted file mode 100644 index 1a7a5ea..0000000 --- a/frontend/src/modules/TodoList/index.js +++ /dev/null @@ -1,5 +0,0 @@ -const TodoPage = (props) => { - return
; -}; - -export default TodoPage; diff --git a/frontend/src/reducers/config.js b/frontend/src/reducers/config.js deleted file mode 100644 index 87cfb4a..0000000 --- a/frontend/src/reducers/config.js +++ /dev/null @@ -1,28 +0,0 @@ -import { createAction, createAsyncAction } from './utils'; -import { createReducer } from '@reduxjs/toolkit'; - -const actions = { - refresh: 'LOCAL_STORAGE_REFRESH' -}; - -export const getConfigValue = createAsyncAction((dispatch, getState, config, key) => { - const payload = { - key: key, - value: JSON.parse(localStorage.getItem(key)) || undefined - }; - return dispatch(refreshConfigValue(payload)); -}); -export const setConfigValue = createAsyncAction((dispatch, getState, config, key, value) => { - localStorage.setItem(key, JSON.stringify(value)); - return dispatch(refreshConfigValue({ key: key, value: value })); -}); - -export const refreshConfigValue = createAction(actions.refresh, (payload) => { - return payload; -}); - -export default createReducer({}, (builder) => { - builder.addDefaultCase((state, action) => { - state[action.payload?.key] = action.payload?.value; - }); -}); diff --git a/frontend/src/reducers/index.js b/frontend/src/reducers/index.js deleted file mode 100644 index e8c5219..0000000 --- a/frontend/src/reducers/index.js +++ /dev/null @@ -1,10 +0,0 @@ -import { combineReducers } from '@reduxjs/toolkit'; -import LocalStorageReducer from './localStorage'; -import LoginReducer from './login'; -import ConfigReducer from './config'; - -export default combineReducers({ - localStorage: LocalStorageReducer, - login: LoginReducer, - config: ConfigReducer -}); diff --git a/frontend/src/reducers/localStorage.js b/frontend/src/reducers/localStorage.js deleted file mode 100644 index 9eff4bf..0000000 --- a/frontend/src/reducers/localStorage.js +++ /dev/null @@ -1,28 +0,0 @@ -import { createAction, createAsyncAction } from './utils'; -import { createReducer } from '@reduxjs/toolkit'; - -const actions = { - refresh: 'LOCAL_STORAGE_REFRESH' -}; - -export const readLocalStorage = createAsyncAction((dispatch, getState, config, key) => { - const payload = { - key: key, - value: JSON.parse(localStorage.getItem(key)) || undefined - }; - return dispatch(refreshLocalStorage(payload)); -}); -export const updateLocalStorage = createAsyncAction((dispatch, getState, config, key, value) => { - localStorage.setItem(key, JSON.stringify(value)); - return dispatch(refreshLocalStorage({ key: key, value: value })); -}); - -export const refreshLocalStorage = createAction(actions.refresh, (payload) => { - return payload; -}); - -export default createReducer({}, (builder) => { - builder.addDefaultCase((state, action) => { - state[action.payload?.key] = action.payload?.value; - }); -}); diff --git a/frontend/src/reducers/login.js b/frontend/src/reducers/login.js deleted file mode 100644 index 79e3ea3..0000000 --- a/frontend/src/reducers/login.js +++ /dev/null @@ -1,120 +0,0 @@ -import axios from 'axios'; -import { createAction, createAsyncAction } from './utils'; -import { createReducer } from '@reduxjs/toolkit'; -import { updateLocalStorage } from './localStorage'; - -const actions = { - update: 'UPDATE_LOGIN_DETAILS' -}; - -const updateLoginDetails = createAction(actions.update, (payload) => { - return payload; -}); - -export const login = createAsyncAction((dispatch, getState, config, email, password) => { - axios - .post(`${config.apiUrl}/user/login`, { - email: email, - password: password - }) - .then( - (success) => { - console.error('success', success); - dispatch( - updateLoginDetails({ - id: success.data['userid'], - token: success.data['session_token'], - error: false - }) - ); - dispatch( - updateLocalStorage('userDetails', { - id: success.data['userid'], - token: success.data['session_token'] - }) - ); - window.location.pathname = '/'; - }, - (reject) => { - console.error(reject); - dispatch( - updateLoginDetails({ - id: undefined, - token: undefined, - error: true - }) - ); - dispatch( - updateLocalStorage('userDetails', { - id: undefined, - token: undefined - }) - ); - } - ); -}); - -export const signup = createAsyncAction((dispatch, getState, config, email, password) => { - axios - .post(`${config.apiUrl}/user/signup`, { - email: email, - password: password - }) - .then( - (success) => { - console.error('success', success); - window.location.pathname = '/login'; - }, - (reject) => { - console.error(reject); - } - ); -}); - -export const forgotPassword = createAsyncAction((dispatch, getState, config, email) => {}); - -export const logout = createAsyncAction((dispatch, getState, config) => { - const details = getState().login; - axios - .post(`${config.apiUrl}/user/logout`, { - userid: details.id, - session_token: details.token - }) - .then( - (success) => { - dispatch( - updateLoginDetails({ - id: undefined, - token: undefined, - error: false - }) - ); - dispatch( - updateLocalStorage('userDetails', { - id: undefined, - token: undefined - }) - ); - - window.location.pathname = '/login'; - }, - (reject) => { - console.warn(reject); - console.warn('could not log out.'); - } - ); -}); - -export default createReducer( - { - id: undefined, - token: undefined, - error: false - }, - (builder) => { - builder.addCase(actions.update, (state, action) => { - console.error(state, action); - state = { ...state, ...action?.payload }; - }); - } -); diff --git a/frontend/src/reducers/utils.js b/frontend/src/reducers/utils.js deleted file mode 100644 index 29de8b4..0000000 --- a/frontend/src/reducers/utils.js +++ /dev/null @@ -1,16 +0,0 @@ -export const createAction = (type, payload) => { - return (...args) => { - return { - type: type, - payload: payload(...args) - }; - }; -}; - -export const createAsyncAction = (payload) => { - return (...args) => { - return function (dispatch, getState, config) { - payload(dispatch, getState, getState()?.config, ...args); - }; - }; -}; diff --git a/frontend/src/reportWebVitals.js b/frontend/src/reportWebVitals.js deleted file mode 100644 index 5253d3a..0000000 --- a/frontend/src/reportWebVitals.js +++ /dev/null @@ -1,13 +0,0 @@ -const reportWebVitals = onPerfEntry => { - if (onPerfEntry && onPerfEntry instanceof Function) { - import('web-vitals').then(({ getCLS, getFID, getFCP, getLCP, getTTFB }) => { - getCLS(onPerfEntry); - getFID(onPerfEntry); - getFCP(onPerfEntry); - getLCP(onPerfEntry); - getTTFB(onPerfEntry); - }); - } -}; - -export default reportWebVitals; diff --git a/frontend/src/setupTests.js b/frontend/src/setupTests.js deleted file mode 100644 index 8f2609b..0000000 --- a/frontend/src/setupTests.js +++ /dev/null @@ -1,5 +0,0 @@ -// jest-dom adds custom jest matchers for asserting on DOM nodes. -// allows you to do things like: -// expect(element).toHaveTextContent(/react/i) -// learn more: https://github.com/testing-library/jest-dom -import '@testing-library/jest-dom'; diff --git a/frontend/src/theme.js b/frontend/src/theme.js deleted file mode 100644 index 3eb926f..0000000 --- a/frontend/src/theme.js +++ /dev/null @@ -1,13 +0,0 @@ -import grey from '@material-ui/core/colors/grey'; - -const theme = { - palette: { - primary: { - main: grey[200] - }, - secondary: { - main: grey[200] - } - } -}; -export default theme;