From 74b9b00de125808894b75a81e5a8c14f4b6b282c Mon Sep 17 00:00:00 2001 From: Kristofers Solo Date: Mon, 27 Jan 2025 17:10:34 +0200 Subject: [PATCH] refactor: make discrete backend and frontend --- .gitignore | 16 +- Cargo.lock | 570 ++++++++++++++++-- Cargo.toml | 109 +--- app/Cargo.toml | 35 -- app/src/components/app.rs | 31 - app/src/components/homepage.rs | 14 - app/src/components/mod.rs | 3 - app/src/components/register.rs | 75 --- app/src/config.rs | 47 -- app/src/lib.rs | 29 - app/src/models/mod.rs | 2 - app/src/models/response.rs | 23 - app/src/models/user/form.rs | 6 - app/src/models/user/mod.rs | 2 - app/src/server_fn/auth.rs | 11 - app/src/server_fn/mod.rs | 1 - app/src/validation.rs | 20 - backend/Cargo.toml | 42 ++ {config => backend/config}/base.toml | 0 {config => backend/config}/local.toml | 0 {config => backend/config}/production.toml | 0 .../migrations}/20250125123853_init.down.sql | 0 .../migrations}/20250125123853_init.up.sql | 0 {server => backend}/src/config.rs | 43 +- {server => backend}/src/db/mod.rs | 0 {server => backend}/src/db/users.rs | 16 +- backend/src/domain/mod.rs | 1 + .../src/domain}/user/error.rs | 0 backend/src/domain/user/mod.rs | 4 + backend/src/domain/user/new_user.rs | 7 + .../src/domain/user}/user_code.rs | 3 +- .../src/domain/user}/username.rs | 28 +- backend/src/error/app.rs | 88 +++ backend/src/error/mod.rs | 1 + backend/src/lib.rs | 8 + {server => backend}/src/main.rs | 22 +- {server => backend}/src/routes/api/mod.rs | 0 backend/src/routes/api/v1/auth.rs | 72 +++ {server => backend}/src/routes/api/v1/mod.rs | 0 .../src/routes/health_check.rs | 0 {server => backend}/src/routes/mod.rs | 20 +- {server => backend}/src/server.rs | 32 +- {server => backend}/src/startup.rs | 4 +- {app => backend}/src/telemetry.rs | 4 +- end2end/playwright.config.ts | 104 ---- end2end/tests/example.spec.ts | 15 - end2end/tsconfig.json | 101 ---- frontend/.gitignore | 14 + frontend/Cargo.toml | 96 ++- README.md => frontend/README.md | 63 +- frontend/end2end/.gitignore | 3 + .../end2end}/package-lock.json | 0 {end2end => frontend/end2end}/package.json | 0 frontend/end2end/playwright.config.ts | 105 ++++ frontend/end2end/tests/example.spec.ts | 9 + frontend/end2end/tsconfig.json | 109 ++++ {public => frontend/public}/favicon.ico | Bin frontend/src/app.rs | 61 ++ frontend/src/lib.rs | 10 +- frontend/src/main.rs | 38 ++ {style => frontend/style}/main.scss | 0 justfile | 9 +- server/Cargo.toml | 41 -- server/src/domain/mod.rs | 3 - server/src/domain/user.rs | 30 - server/src/error/mod.rs | 4 - server/src/error/server.rs | 15 - server/src/error/user.rs | 21 - server/src/fileserv.rs | 44 -- server/src/routes/api/v1/auth.rs | 41 -- 70 files changed, 1309 insertions(+), 1016 deletions(-) delete mode 100644 app/Cargo.toml delete mode 100644 app/src/components/app.rs delete mode 100644 app/src/components/homepage.rs delete mode 100644 app/src/components/mod.rs delete mode 100644 app/src/components/register.rs delete mode 100644 app/src/config.rs delete mode 100644 app/src/lib.rs delete mode 100644 app/src/models/mod.rs delete mode 100644 app/src/models/response.rs delete mode 100644 app/src/models/user/form.rs delete mode 100644 app/src/models/user/mod.rs delete mode 100644 app/src/server_fn/auth.rs delete mode 100644 app/src/server_fn/mod.rs delete mode 100644 app/src/validation.rs create mode 100644 backend/Cargo.toml rename {config => backend/config}/base.toml (100%) rename {config => backend/config}/local.toml (100%) rename {config => backend/config}/production.toml (100%) rename {migrations => backend/migrations}/20250125123853_init.down.sql (100%) rename {migrations => backend/migrations}/20250125123853_init.up.sql (100%) rename {server => backend}/src/config.rs (65%) rename {server => backend}/src/db/mod.rs (100%) rename {server => backend}/src/db/users.rs (68%) create mode 100644 backend/src/domain/mod.rs rename {app/src/models => backend/src/domain}/user/error.rs (100%) create mode 100644 backend/src/domain/user/mod.rs create mode 100644 backend/src/domain/user/new_user.rs rename {server/src/domain => backend/src/domain/user}/user_code.rs (97%) rename {server/src/domain => backend/src/domain/user}/username.rs (70%) create mode 100644 backend/src/error/app.rs create mode 100644 backend/src/error/mod.rs create mode 100644 backend/src/lib.rs rename {server => backend}/src/main.rs (57%) rename {server => backend}/src/routes/api/mod.rs (100%) create mode 100644 backend/src/routes/api/v1/auth.rs rename {server => backend}/src/routes/api/v1/mod.rs (100%) rename {server => backend}/src/routes/health_check.rs (100%) rename {server => backend}/src/routes/mod.rs (72%) rename {server => backend}/src/server.rs (51%) rename {server => backend}/src/startup.rs (81%) rename {app => backend}/src/telemetry.rs (92%) delete mode 100644 end2end/playwright.config.ts delete mode 100644 end2end/tests/example.spec.ts delete mode 100644 end2end/tsconfig.json create mode 100644 frontend/.gitignore rename README.md => frontend/README.md (55%) create mode 100644 frontend/end2end/.gitignore rename {end2end => frontend/end2end}/package-lock.json (100%) rename {end2end => frontend/end2end}/package.json (100%) create mode 100644 frontend/end2end/playwright.config.ts create mode 100644 frontend/end2end/tests/example.spec.ts create mode 100644 frontend/end2end/tsconfig.json rename {public => frontend/public}/favicon.ico (100%) create mode 100644 frontend/src/app.rs create mode 100644 frontend/src/main.rs rename {style => frontend/style}/main.scss (100%) delete mode 100644 server/Cargo.toml delete mode 100644 server/src/domain/mod.rs delete mode 100644 server/src/domain/user.rs delete mode 100644 server/src/error/mod.rs delete mode 100644 server/src/error/server.rs delete mode 100644 server/src/error/user.rs delete mode 100644 server/src/fileserv.rs delete mode 100644 server/src/routes/api/v1/auth.rs diff --git a/.gitignore b/.gitignore index c8b9aff..f6b8b7d 100644 --- a/.gitignore +++ b/.gitignore @@ -1,17 +1,21 @@ # Generated by Cargo # will have compiled files and executables -/target/ +debug/ +target/ pkg +# Remove Cargo.lock from gitignore if creating an executable, leave it for libraries +# More information here https://doc.rust-lang.org/cargo/guide/cargo-toml-vs-cargo-lock.html +Cargo.lock + # These are backup files generated by rustfmt **/*.rs.bk -# node e2e test tools and outputs -node_modules/ -test-results/ -end2end/playwright-report/ -playwright/.cache/ +# MSVC Windows builds of rustc generate these, which store debugging information +*.pdb + bruno/ .env **/*.db + diff --git a/Cargo.lock b/Cargo.lock index b7364e8..b06f0b8 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -93,29 +93,6 @@ version = "1.0.95" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "34ac096ce696dc2fcabef30516bb13c0a68a11d30131d3df6f04711467681b04" -[[package]] -name = "app" -version = "0.1.0" -dependencies = [ - "cfg-if", - "chrono", - "config 0.15.6", - "http", - "leptos", - "leptos_axum", - "leptos_meta", - "leptos_router", - "serde", - "serde-aux", - "thiserror 2.0.11", - "tokio", - "tracing", - "tracing-bunyan-formatter", - "tracing-log 0.2.0", - "tracing-subscriber", - "uuid", -] - [[package]] name = "argon2" version = "0.5.3" @@ -175,6 +152,12 @@ dependencies = [ "num-traits", ] +[[package]] +name = "atomic-waker" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0" + [[package]] name = "attribute-derive" version = "0.10.3" @@ -218,7 +201,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "edca88bc138befd0323b20752846e6587272d3b03b0343c8ea28a6f819e6e71f" dependencies = [ "async-trait", - "axum-core", + "axum-core 0.4.5", "bytes", "futures-util", "http", @@ -227,7 +210,7 @@ dependencies = [ "hyper", "hyper-util", "itoa", - "matchit", + "matchit 0.7.3", "memchr", "mime", "multer", @@ -246,6 +229,40 @@ dependencies = [ "tracing", ] +[[package]] +name = "axum" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6d6fd624c75e18b3b4c6b9caf42b1afe24437daaee904069137d8bab077be8b8" +dependencies = [ + "axum-core 0.5.0", + "bytes", + "form_urlencoded", + "futures-util", + "http", + "http-body", + "http-body-util", + "hyper", + "hyper-util", + "itoa", + "matchit 0.8.4", + "memchr", + "mime", + "percent-encoding", + "pin-project-lite", + "rustversion", + "serde", + "serde_json", + "serde_path_to_error", + "serde_urlencoded", + "sync_wrapper", + "tokio", + "tower", + "tower-layer", + "tower-service", + "tracing", +] + [[package]] name = "axum-core" version = "0.4.5" @@ -267,6 +284,56 @@ dependencies = [ "tracing", ] +[[package]] +name = "axum-core" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df1362f362fd16024ae199c1970ce98f9661bf5ef94b9808fee734bc3698b733" +dependencies = [ + "bytes", + "futures-util", + "http", + "http-body", + "http-body-util", + "mime", + "pin-project-lite", + "rustversion", + "sync_wrapper", + "tower-layer", + "tower-service", + "tracing", +] + +[[package]] +name = "backend" +version = "0.1.0" +dependencies = [ + "anyhow", + "argon2", + "axum 0.8.1", + "chrono", + "config 0.15.6", + "hex", + "password-hash", + "rand", + "secrecy", + "serde", + "serde-aux", + "serde_json", + "sqlx", + "thiserror 2.0.11", + "tokio", + "tower", + "tower-http", + "tracing", + "tracing-bunyan-formatter", + "tracing-log 0.2.0", + "tracing-subscriber", + "unicode-segmentation", + "uuid", + "validator", +] + [[package]] name = "backtrace" version = "0.3.74" @@ -449,6 +516,16 @@ dependencies = [ "winnow", ] +[[package]] +name = "console_error_panic_hook" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a06aeb73f470f66dcdbf7223caeebb85984942f22f1adb2a088cf9668146bbbc" +dependencies = [ + "cfg-if", + "wasm-bindgen", +] + [[package]] name = "const-oid" version = "0.9.6" @@ -490,6 +567,16 @@ dependencies = [ "unicode-segmentation", ] +[[package]] +name = "core-foundation" +version = "0.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91e195e091a93c46f7102ec7818a2aa394e1e1771c3ab4825963fa03e45afb8f" +dependencies = [ + "core-foundation-sys", + "libc", +] + [[package]] name = "core-foundation-sys" version = "0.8.7" @@ -748,6 +835,21 @@ version = "0.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a0d2fde1f7b3d48b8395d5f2de76c18a528bd6a9cdde438df747bfcba3e05d6f" +[[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.2.1" @@ -761,9 +863,15 @@ dependencies = [ name = "frontend" version = "0.1.0" dependencies = [ - "app", + "axum 0.7.9", + "console_error_panic_hook", "leptos", - "tracing", + "leptos_axum", + "leptos_meta", + "leptos_router", + "reqwest", + "serde", + "tokio", "wasm-bindgen", ] @@ -937,6 +1045,25 @@ version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "493913a18c0d7bebb75127a26a432162c59edbe06f6cf712001e3e769345e8b5" +[[package]] +name = "h2" +version = "0.4.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ccae279728d634d083c00f6099cb58f01cc99c145b84b8be2f6c74618d79922e" +dependencies = [ + "atomic-waker", + "bytes", + "fnv", + "futures-core", + "futures-sink", + "http", + "indexmap", + "slab", + "tokio", + "tokio-util", + "tracing", +] + [[package]] name = "hashbrown" version = "0.14.5" @@ -1104,6 +1231,7 @@ dependencies = [ "bytes", "futures-channel", "futures-util", + "h2", "http", "http-body", "httparse", @@ -1112,6 +1240,40 @@ dependencies = [ "pin-project-lite", "smallvec", "tokio", + "want", +] + +[[package]] +name = "hyper-rustls" +version = "0.27.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2d191583f3da1305256f22463b9bb0471acad48a4e534a5218b9963e9c1f59b2" +dependencies = [ + "futures-util", + "http", + "hyper", + "hyper-util", + "rustls", + "rustls-pki-types", + "tokio", + "tokio-rustls", + "tower-service", +] + +[[package]] +name = "hyper-tls" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "70206fc6890eaca9fde8a0bf71caa2ddfc9fe045ac9e5c70df101a7dbde866e0" +dependencies = [ + "bytes", + "http-body-util", + "hyper", + "hyper-util", + "native-tls", + "tokio", + "tokio-native-tls", + "tower-service", ] [[package]] @@ -1121,13 +1283,16 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "df2dcfbe0677734ab2f3ffa7fa7bfd4706bfdc1ef393f2ee30184aed67e631b4" dependencies = [ "bytes", + "futures-channel", "futures-util", "http", "http-body", "hyper", "pin-project-lite", + "socket2", "tokio", "tower-service", + "tracing", ] [[package]] @@ -1317,6 +1482,12 @@ dependencies = [ "rustversion", ] +[[package]] +name = "ipnet" +version = "2.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "469fb0b9cefa57e3ef31275ee7cacb78f2fdca44e4765491884a2b119d4eb130" + [[package]] name = "iri-string" version = "0.7.7" @@ -1402,7 +1573,6 @@ dependencies = [ "tachys", "thiserror 2.0.11", "throw_error", - "tracing", "typed-builder", "typed-builder-macro", "wasm-bindgen", @@ -1416,7 +1586,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "43b613d5784037baee42a11d21bc263adfc1a55e416556a3d5bfe39c7b87fadf" dependencies = [ "any_spawner", - "axum", + "axum 0.7.9", "dashmap", "futures", "hydration_context", @@ -1457,7 +1627,6 @@ dependencies = [ "reactive_graph", "send_wrapper", "tachys", - "tracing", "wasm-bindgen", "web-sys", ] @@ -1514,7 +1683,6 @@ dependencies = [ "rstml", "server_fn_macro", "syn", - "tracing", "uuid", ] @@ -1588,7 +1756,6 @@ dependencies = [ "serde_json", "server_fn", "tachys", - "tracing", ] [[package]] @@ -1685,6 +1852,12 @@ version = "0.7.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0e7465ac9959cc2b1404e8e2367b43684a6d13790fe23056cc8c6c5a6b7bcb94" +[[package]] +name = "matchit" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47e1ffaa40ddd1f3ed91f717a33c8c0ee23fff369e3aa8772b9605cc1d22f4c3" + [[package]] name = "md-5" version = "0.10.6" @@ -1760,6 +1933,23 @@ dependencies = [ "version_check", ] +[[package]] +name = "native-tls" +version = "0.2.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0dab59f8e050d5df8e4dd87d9206fb6f65a483e20ac9fda365ade4fab353196c" +dependencies = [ + "libc", + "log", + "openssl", + "openssl-probe", + "openssl-sys", + "schannel", + "security-framework", + "security-framework-sys", + "tempfile", +] + [[package]] name = "next_tuple" version = "0.1.0" @@ -1874,6 +2064,50 @@ version = "1.20.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1261fe7e33c73b354eab43b1273a57c8f967d0391e80353e51f764ac02cf6775" +[[package]] +name = "openssl" +version = "0.10.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f5e534d133a060a3c19daec1eb3e98ec6f4685978834f2dbadfe2ec215bab64e" +dependencies = [ + "bitflags", + "cfg-if", + "foreign-types", + "libc", + "once_cell", + "openssl-macros", + "openssl-sys", +] + +[[package]] +name = "openssl-macros" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "openssl-probe" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d05e27ee213611ffe7d6348b942e8f942b37114c00cc03cec254295a4a17852e" + +[[package]] +name = "openssl-sys" +version = "0.9.104" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "45abf306cbf99debc8195b66b7346498d7b10c210de50418b5ccd7ceba08c741" +dependencies = [ + "cc", + "libc", + "pkg-config", + "vcpkg", +] + [[package]] name = "or_poisoned" version = "0.1.0" @@ -2171,7 +2405,6 @@ dependencies = [ "serde", "slotmap", "thiserror 2.0.11", - "tracing", "web-sys", ] @@ -2256,6 +2489,65 @@ version = "0.8.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c" +[[package]] +name = "reqwest" +version = "0.12.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "43e734407157c3c2034e0258f5e4473ddb361b1e85f95a66690d67264d7cd1da" +dependencies = [ + "base64", + "bytes", + "encoding_rs", + "futures-core", + "futures-util", + "h2", + "http", + "http-body", + "http-body-util", + "hyper", + "hyper-rustls", + "hyper-tls", + "hyper-util", + "ipnet", + "js-sys", + "log", + "mime", + "native-tls", + "once_cell", + "percent-encoding", + "pin-project-lite", + "rustls-pemfile", + "serde", + "serde_json", + "serde_urlencoded", + "sync_wrapper", + "system-configuration", + "tokio", + "tokio-native-tls", + "tower", + "tower-service", + "url", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", + "windows-registry", +] + +[[package]] +name = "ring" +version = "0.17.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c17fa4cb658e3583423e915b9f3acc01cceaee1860e33d59ebae66adc3a2dc0d" +dependencies = [ + "cc", + "cfg-if", + "getrandom", + "libc", + "spin", + "untrusted", + "windows-sys 0.52.0", +] + [[package]] name = "rsa" version = "0.9.7" @@ -2316,6 +2608,45 @@ dependencies = [ "windows-sys 0.59.0", ] +[[package]] +name = "rustls" +version = "0.23.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f287924602bf649d949c63dc8ac8b235fa5387d394020705b80c4eb597ce5b8" +dependencies = [ + "once_cell", + "rustls-pki-types", + "rustls-webpki", + "subtle", + "zeroize", +] + +[[package]] +name = "rustls-pemfile" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dce314e5fee3f39953d46bb63bb8a46d40c2f8fb7cc5a3b6cab2bde9721d6e50" +dependencies = [ + "rustls-pki-types", +] + +[[package]] +name = "rustls-pki-types" +version = "1.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d2bf47e6ff922db3825eb750c4e2ff784c6ff8fb9e13046ef6a1d1c5401b0b37" + +[[package]] +name = "rustls-webpki" +version = "0.102.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "64ca1bc8749bd4cf37b5ce386cc146580777b4e8572c7b97baf22c83f444bee9" +dependencies = [ + "ring", + "rustls-pki-types", + "untrusted", +] + [[package]] name = "rustversion" version = "1.0.19" @@ -2337,6 +2668,15 @@ dependencies = [ "winapi-util", ] +[[package]] +name = "schannel" +version = "0.1.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1f29ebaa345f945cec9fbbc532eb307f0fdad8161f281b6369539c8d84876b3d" +dependencies = [ + "windows-sys 0.59.0", +] + [[package]] name = "scopeguard" version = "1.2.0" @@ -2353,6 +2693,29 @@ dependencies = [ "zeroize", ] +[[package]] +name = "security-framework" +version = "2.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "897b2245f0b511c87893af39b033e5ca9cce68824c4d7e7630b5a1d339658d02" +dependencies = [ + "bitflags", + "core-foundation", + "core-foundation-sys", + "libc", + "security-framework-sys", +] + +[[package]] +name = "security-framework-sys" +version = "2.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "49db231d56a190491cb4aeda9527f1ad45345af50b0851622a7adb8c03b01c32" +dependencies = [ + "core-foundation-sys", + "libc", +] + [[package]] name = "send_wrapper" version = "0.6.0" @@ -2447,41 +2810,13 @@ dependencies = [ "serde", ] -[[package]] -name = "server" -version = "0.1.0" -dependencies = [ - "app", - "argon2", - "axum", - "config 0.15.6", - "hex", - "leptos", - "leptos_axum", - "leptos_router", - "password-hash", - "rand", - "secrecy", - "serde", - "serde-aux", - "sqlx", - "thiserror 2.0.11", - "tokio", - "tower", - "tower-http", - "tracing", - "tracing-log 0.2.0", - "unicode-segmentation", - "uuid", -] - [[package]] name = "server_fn" version = "0.7.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f5dd7fcccd3ef2081da086c1f8595b506627abbbbc9f64be0141d2251219570e" dependencies = [ - "axum", + "axum 0.7.9", "bytes", "const_format", "dashmap", @@ -2884,6 +3219,9 @@ name = "sync_wrapper" version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0bf256ce5efdfa370213c1dabab5935a12e49f2c58d15e9eac2870d3b4f27263" +dependencies = [ + "futures-core", +] [[package]] name = "synstructure" @@ -2896,6 +3234,27 @@ dependencies = [ "syn", ] +[[package]] +name = "system-configuration" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c879d448e9d986b661742763247d3693ed13609438cf3d006f51f5368a5ba6b" +dependencies = [ + "bitflags", + "core-foundation", + "system-configuration-sys", +] + +[[package]] +name = "system-configuration-sys" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e1d1b10ced5ca923a1fcb8d03e96b8d3268065d724548c0211415ff6ac6bac4" +dependencies = [ + "core-foundation-sys", + "libc", +] + [[package]] name = "tachys" version = "0.1.4" @@ -2924,7 +3283,6 @@ dependencies = [ "send_wrapper", "slotmap", "throw_error", - "tracing", "wasm-bindgen", "web-sys", ] @@ -3086,6 +3444,26 @@ dependencies = [ "syn", ] +[[package]] +name = "tokio-native-tls" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbae76ab933c85776efabc971569dd6119c580d8f5d448769dec1764bf796ef2" +dependencies = [ + "native-tls", + "tokio", +] + +[[package]] +name = "tokio-rustls" +version = "0.26.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5f6d0975eaace0cf0fcadee4e4aaa5da15b5c079146f2cffb67c113be122bf37" +dependencies = [ + "rustls", + "tokio", +] + [[package]] name = "tokio-stream" version = "0.1.17" @@ -3297,6 +3675,12 @@ dependencies = [ "tracing-log 0.2.0", ] +[[package]] +name = "try-lock" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" + [[package]] name = "typed-builder" version = "0.20.0" @@ -3368,6 +3752,12 @@ version = "0.2.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853" +[[package]] +name = "untrusted" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" + [[package]] name = "url" version = "2.5.4" @@ -3407,6 +3797,21 @@ dependencies = [ "serde", ] +[[package]] +name = "validator" +version = "0.20.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "43fb22e1a008ece370ce08a3e9e4447a910e92621bb49b85d6e48a45397e7cfa" +dependencies = [ + "idna", + "once_cell", + "regex", + "serde", + "serde_derive", + "serde_json", + "url", +] + [[package]] name = "valuable" version = "0.1.1" @@ -3435,6 +3840,15 @@ dependencies = [ "winapi-util", ] +[[package]] +name = "want" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bfa7760aed19e106de2c7c0b581b509f2f25d3dacaf737cb82ac61bc6d760b0e" +dependencies = [ + "try-lock", +] + [[package]] name = "wasi" version = "0.11.0+wasi-snapshot-preview1" @@ -3591,6 +4005,36 @@ dependencies = [ "windows-targets 0.52.6", ] +[[package]] +name = "windows-registry" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e400001bb720a623c1c69032f8e3e4cf09984deec740f007dd2b03ec864804b0" +dependencies = [ + "windows-result", + "windows-strings", + "windows-targets 0.52.6", +] + +[[package]] +name = "windows-result" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d1043d8214f791817bab27572aaa8af63732e11bf84aa21a45a78d6c317ae0e" +dependencies = [ + "windows-targets 0.52.6", +] + +[[package]] +name = "windows-strings" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4cd9b125c486025df0eabcb585e62173c6c9eddcec5d117d3b6e8c30e2ee4d10" +dependencies = [ + "windows-result", + "windows-targets 0.52.6", +] + [[package]] name = "windows-sys" version = "0.48.0" diff --git a/Cargo.toml b/Cargo.toml index 5997767..9752738 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,117 +1,24 @@ [workspace] +members = ["frontend", "backend"] resolver = "2" -members = ["app", "frontend", "server"] - -# need to be applied only to wasm build -[profile.release] -codegen-units = 1 -lto = true -opt-level = "z" [workspace.dependencies] -leptos = { version = "0.7", features = ["nightly", "tracing"] } -leptos_meta = { version = "0.7" } -leptos_router = { version = "0.7", features = ["nightly"] } -leptos_axum = { version = "0.7" } - -axum = "0.7" cfg-if = "1" http = "1" -tracing = "0.1" -tracing-subscriber = { version = "0.3", features = ["registry", "env-filter"] } -tracing-bunyan-formatter = { version = "0.3", default-features = false } -tracing-log = "0.2" thiserror = "2.0" +wasm-bindgen = "=0.2.100" tokio = { version = "1.43", features = ["rt", "macros", "tracing"] } +serde = { version = "1", features = ["derive"] } tower = { version = "0.5", features = ["full"] } tower-http = { version = "0.6", features = ["full"] } -wasm-bindgen = "=0.2.100" uuid = { version = "1.12", features = ["v4", "serde"] } -serde = { version = "1", features = ["derive"] } -chrono = { version = "0.4", features = ["serde", "clock"] } -secrecy = { version = "0.10", features = ["serde"] } -validator = "0.20" -config = { version = "0.15", features = ["toml"], default-features = false } -serde-aux = "4" -unicode-segmentation = "1" -rand = "0.8" -argon2 = "0.5" -password-hash = "0.5" -hex = "0.4" -# See https://github.com/leptos-rs/cargo-leptos for documentation of all the parameters. - -# A leptos project defines which workspace members -# that are used together frontend (lib) & server (bin) -[[workspace.metadata.leptos]] -# this name is used for the wasm, js and css file names -name = "echoes-of-ascension" - -# the package in the workspace that contains the server binary (binary crate) -bin-package = "server" - -# the package in the workspace that contains the frontend wasm binary (library crate) -lib-package = "frontend" - -# The site root folder is where cargo-leptos generate all output. WARNING: all content of this folder will be erased on a rebuild. Use it in your server setup. -site-root = "target/site" - -# The site-root relative folder where all compiled output (JS, WASM and CSS) is written -# Defaults to pkg -site-pkg-dir = "pkg" - -# [Optional] The source CSS file. If it ends with .sass or .scss then it will be compiled by dart-sass into CSS. The CSS is optimized by Lightning CSS before being written to //app.css -style-file = "style/main.scss" - -# Assets source dir. All files found here will be copied and synchronized to site-root. -# The assets-dir cannot have a sub directory with the same name/path as site-pkg-dir. -# -# Optional. Env: LEPTOS_ASSETS_DIR. -assets-dir = "public" - -# The IP and port (ex: 127.0.0.1:3000) where the server serves the content. Use it in your server setup. -site-addr = "127.0.0.1:3000" - -# The port to use for automatic reload monitoring -reload-port = 3001 - -# [Optional] Command to use when running end2end tests. It will run in the end2end dir. -# [Windows] for non-WSL use "npx.cmd playwright test" -# This binary name can be checked in Powershell with Get-Command npx -end2end-cmd = "npx playwright test" -end2end-dir = "end2end" - -# The browserlist query used for optimizing the CSS. -browserquery = "defaults" - -# Set by cargo-leptos watch when building with that tool. Controls whether autoreload JS will be included in the head -watch = false - -# The environment Leptos will run in, usually either "DEV" or "PROD" -env = "DEV" - -# The features to use when compiling the bin target -# -# Optional. Can be over-ridden with the command line parameter --bin-features -bin-features = [] - -# If the --no-default-features flag should be used when compiling the bin target -# -# Optional. Defaults to false. -bin-default-features = false - -# The features to use when compiling the lib target -# -# Optional. Can be over-ridden with the command line parameter --lib-features -lib-features = [] - -# If the --no-default-features flag should be used when compiling the lib target -# -# Optional. Defaults to false. -lib-default-features = false +[profile.release] +opt-level = "z" +lto = true +codegen-units = 1 [workspace.lints.clippy] -pedantic = "warn" nursery = "warn" +pedantic = "warn" unwrap_used = "warn" -expect_used = "warn" diff --git a/app/Cargo.toml b/app/Cargo.toml deleted file mode 100644 index 83428ff..0000000 --- a/app/Cargo.toml +++ /dev/null @@ -1,35 +0,0 @@ -[package] -name = "app" -version = "0.1.0" -edition = "2021" - -# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html - -[dependencies] -leptos.workspace = true -leptos_meta.workspace = true -leptos_router.workspace = true -leptos_axum = { workspace = true, optional = true } - -http.workspace = true -cfg-if.workspace = true -thiserror.workspace = true - -tracing.workspace = true -tracing-subscriber.workspace = true -tracing-bunyan-formatter.workspace = true -tracing-log.workspace = true -uuid.workspace = true -tokio.workspace = true -chrono.workspace = true -serde.workspace = true -serde-aux.workspace = true -config.workspace = true - -[features] -default = [] -hydrate = ["leptos/hydrate"] -ssr = ["leptos/ssr", "leptos_meta/ssr", "leptos_router/ssr", "dep:leptos_axum"] - -[lints] -workspace = true diff --git a/app/src/components/app.rs b/app/src/components/app.rs deleted file mode 100644 index 923ef0a..0000000 --- a/app/src/components/app.rs +++ /dev/null @@ -1,31 +0,0 @@ -use leptos::prelude::*; -use leptos_meta::{provide_meta_context, Stylesheet, Title}; -use leptos_router::{ - components::{Route, Router, Routes}, - StaticSegment, -}; - -use crate::components::{homepage::HomePage, register::RegisterPage}; - -#[component] -pub fn App() -> impl IntoView { - // Provides context that manages stylesheets, titles, meta tags, etc. - provide_meta_context(); - - view! { - - - // sets the document title - - - // content for this welcome page - <Router> - <main> - <Routes fallback=|| "Page not found.".into_view()> - <Route path=StaticSegment("") view=HomePage /> - <Route path=StaticSegment("/register") view=RegisterPage /> - </Routes> - </main> - </Router> - } -} diff --git a/app/src/components/homepage.rs b/app/src/components/homepage.rs deleted file mode 100644 index 140458b..0000000 --- a/app/src/components/homepage.rs +++ /dev/null @@ -1,14 +0,0 @@ -use leptos::prelude::*; - -/// Renders the home page of your application. -#[component] -pub fn HomePage() -> impl IntoView { - // Creates a reactive value to update the button - let count = RwSignal::new(0); - let on_click = move |_| *count.write() += 1; - - view! { - <h1>"Welcome to Leptos!"</h1> - <button on:click=on_click>"Click Me: " {count}</button> - } -} diff --git a/app/src/components/mod.rs b/app/src/components/mod.rs deleted file mode 100644 index 3dd0bf8..0000000 --- a/app/src/components/mod.rs +++ /dev/null @@ -1,3 +0,0 @@ -pub mod app; -mod homepage; -mod register; diff --git a/app/src/components/register.rs b/app/src/components/register.rs deleted file mode 100644 index bb9411a..0000000 --- a/app/src/components/register.rs +++ /dev/null @@ -1,75 +0,0 @@ -use leptos::prelude::*; - -use crate::{models::user::form::RegisterUserForm, server_fn::auth::register_user}; - -#[component] -pub fn RegisterPage() -> impl IntoView { - let (username, set_username) = signal(String::new()); - let register_action = Action::new(|input: &RegisterUserForm| { - let input = input.clone(); - async move { register_user(input).await } - }); - - let response = register_action.value(); - let pending = register_action.pending(); - - view! { - <div class="container"> - <form on:submit=move |ev| { - ev.prevent_default(); - register_action - .dispatch(RegisterUserForm { - username: username.get(), - }); - }> - <h1>"Register"</h1> - <div class="input-group"> - <label for="username">"Username"</label> - <input - type="text" - id="username" - name="username" - on:input=move |ev| { - set_username(event_target_value(&ev)); - } - prop:value=username - /> - // <div class="error"> - // {move || { - // response - // .get() - // .and_then(|result| result.err()) - // .map(|err| err.to_string()) - // }} - // </div> - </div> - <button type="submit" disabled=pending> - {move || { - response - .with(|r| { - r.as_ref() - .and_then(|result| result.as_ref().err()) - .map(|err| err.to_string()) - }) - }} - "Register" - </button> - </form> - {move || { - response - .with(|r| { - r.as_ref() - .and_then(|result| result.as_ref().ok()) - .map(|res| { - view! { - <div class="success"> - <p>"Reistration successful!"</p> - <p>"Your code ir: "{res.code.clone()}</p> - </div> - } - }) - }) - }} - </div> - } -} diff --git a/app/src/config.rs b/app/src/config.rs deleted file mode 100644 index b6175b4..0000000 --- a/app/src/config.rs +++ /dev/null @@ -1,47 +0,0 @@ -use serde::Deserialize; -use serde_aux::field_attributes::deserialize_number_from_string; -use std::{fmt::Display, str::FromStr}; - -#[derive(Debug, Deserialize, Clone)] -pub struct ApplicationSettings { - #[serde(deserialize_with = "deserialize_number_from_string")] - pub port: u16, - pub host: String, -} - -#[derive(Debug, Clone)] -pub enum Environment { - Local, - Production, -} - -impl Display for Environment { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - match self { - Environment::Local => write!(f, "local"), - Environment::Production => write!(f, "production"), - } - } -} - -impl TryFrom<String> for Environment { - type Error = String; - fn try_from(value: String) -> Result<Self, Self::Error> { - match value.to_lowercase().as_str() { - "local" => Ok(Self::Local), - "production" => Ok(Self::Production), - other => Err(format!( - "{} is not supported environment. \ - Use either `local` or `production`.", - other - )), - } - } -} - -impl FromStr for Environment { - type Err = String; - fn from_str(s: &str) -> Result<Self, Self::Err> { - s.to_owned().try_into() - } -} diff --git a/app/src/lib.rs b/app/src/lib.rs deleted file mode 100644 index 136ce3b..0000000 --- a/app/src/lib.rs +++ /dev/null @@ -1,29 +0,0 @@ -pub mod components; -pub mod config; -pub mod models; -pub mod server_fn; -pub mod telemetry; -pub mod validation; - -pub use components::app::App; - -use leptos::prelude::*; -use leptos_meta::MetaTags; - -pub fn shell(options: LeptosOptions) -> impl IntoView { - view! { - <!DOCTYPE html> - <html lang="en"> - <head> - <meta charset="utf-8" /> - <meta name="viewport" content="width=device-width, initial-scale=1" /> - <AutoReload options=options.clone() /> - <HydrationScripts options /> - <MetaTags /> - </head> - <body> - <App /> - </body> - </html> - } -} diff --git a/app/src/models/mod.rs b/app/src/models/mod.rs deleted file mode 100644 index 601225e..0000000 --- a/app/src/models/mod.rs +++ /dev/null @@ -1,2 +0,0 @@ -pub mod response; -pub mod user; diff --git a/app/src/models/response.rs b/app/src/models/response.rs deleted file mode 100644 index 8230504..0000000 --- a/app/src/models/response.rs +++ /dev/null @@ -1,23 +0,0 @@ -use serde::{Deserialize, Serialize}; - -#[derive(Debug, Serialize, Deserialize)] -pub struct RegisterResponse { - pub username: String, - pub code: String, -} - -#[derive(Debug, Serialize, Deserialize)] -pub struct ErrorResponse { - pub error: String, -} - -impl<T> From<T> for ErrorResponse -where - T: Into<String>, -{ - fn from(value: T) -> Self { - Self { - error: value.into(), - } - } -} diff --git a/app/src/models/user/form.rs b/app/src/models/user/form.rs deleted file mode 100644 index a07978a..0000000 --- a/app/src/models/user/form.rs +++ /dev/null @@ -1,6 +0,0 @@ -use serde::{Deserialize, Serialize}; - -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct RegisterUserForm { - pub username: String, -} diff --git a/app/src/models/user/mod.rs b/app/src/models/user/mod.rs deleted file mode 100644 index 4d8543d..0000000 --- a/app/src/models/user/mod.rs +++ /dev/null @@ -1,2 +0,0 @@ -pub mod error; -pub mod form; diff --git a/app/src/server_fn/auth.rs b/app/src/server_fn/auth.rs deleted file mode 100644 index 44ee8d4..0000000 --- a/app/src/server_fn/auth.rs +++ /dev/null @@ -1,11 +0,0 @@ -use leptos::{prelude::*, server}; - -use crate::models::{response::RegisterResponse, user::form::RegisterUserForm}; - -#[server(RegisterUser, "/register")] -pub async fn register_user(form: RegisterUserForm) -> Result<RegisterResponse, ServerFnError> { - Ok(RegisterResponse { - username: form.username, - code: String::new(), - }) -} diff --git a/app/src/server_fn/mod.rs b/app/src/server_fn/mod.rs deleted file mode 100644 index 0e4a05d..0000000 --- a/app/src/server_fn/mod.rs +++ /dev/null @@ -1 +0,0 @@ -pub mod auth; diff --git a/app/src/validation.rs b/app/src/validation.rs deleted file mode 100644 index ca34130..0000000 --- a/app/src/validation.rs +++ /dev/null @@ -1,20 +0,0 @@ -use thiserror::Error; - -#[derive(Debug, Error)] -pub enum ValidationError { - #[error("Field cannot be empty: {0}")] - Empty(String), - - #[error("Field too short: {0} (minimum {1} characters)")] - TooShort(String, usize), - - #[error("Field too long: {0} (maximum {1} characters)")] - TooLong(String, usize), - - #[error("Invalid format: {0}")] - InvalidFormat(String), -} - -pub trait Validate { - fn validate(&self) -> Result<(), ValidationError>; -} diff --git a/backend/Cargo.toml b/backend/Cargo.toml new file mode 100644 index 0000000..daf3143 --- /dev/null +++ b/backend/Cargo.toml @@ -0,0 +1,42 @@ +[package] +name = "backend" +version = "0.1.0" +edition = "2021" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +thiserror.workspace = true +axum = "0.8" +tokio = { workspace = true, features = ["rt-multi-thread"] } +tower.workspace = true +tower-http = { workspace = true, features = ["cors"] } +serde.workspace = true +serde_json = "1" +uuid.workspace = true +sqlx = { version = "0.8", features = [ + "runtime-tokio", + "macros", + "postgres", + "uuid", + "chrono", + "migrate", +] } +chrono = { version = "0.4", features = ["serde", "clock"] } +secrecy = { version = "0.10", features = ["serde"] } +validator = "0.20" +config = { version = "0.15", features = ["toml"], default-features = false } +serde-aux = "4" +unicode-segmentation = "1" +rand = "0.8" +argon2 = "0.5" +password-hash = "0.5" +hex = "0.4" +tracing = "0.1" +tracing-subscriber = { version = "0.3", features = ["registry", "env-filter"] } +tracing-bunyan-formatter = { version = "0.3", default-features = false } +tracing-log = "0.2" +anyhow = "1" + +[lints] +workspace = true diff --git a/config/base.toml b/backend/config/base.toml similarity index 100% rename from config/base.toml rename to backend/config/base.toml diff --git a/config/local.toml b/backend/config/local.toml similarity index 100% rename from config/local.toml rename to backend/config/local.toml diff --git a/config/production.toml b/backend/config/production.toml similarity index 100% rename from config/production.toml rename to backend/config/production.toml diff --git a/migrations/20250125123853_init.down.sql b/backend/migrations/20250125123853_init.down.sql similarity index 100% rename from migrations/20250125123853_init.down.sql rename to backend/migrations/20250125123853_init.down.sql diff --git a/migrations/20250125123853_init.up.sql b/backend/migrations/20250125123853_init.up.sql similarity index 100% rename from migrations/20250125123853_init.up.sql rename to backend/migrations/20250125123853_init.up.sql diff --git a/server/src/config.rs b/backend/src/config.rs similarity index 65% rename from server/src/config.rs rename to backend/src/config.rs index 97aa0e2..b75de84 100644 --- a/server/src/config.rs +++ b/backend/src/config.rs @@ -1,4 +1,5 @@ -use app::config::{ApplicationSettings, Environment}; +use std::fmt::Display; + use secrecy::{ExposeSecret, SecretString}; use serde::Deserialize; use serde_aux::field_attributes::deserialize_number_from_string; @@ -24,7 +25,21 @@ pub struct DatabaseSettings { pub require_ssl: bool, } +#[derive(Debug, Deserialize, Clone)] +pub struct ApplicationSettings { + #[serde(deserialize_with = "deserialize_number_from_string")] + pub port: u16, + pub host: String, +} + +#[derive(Debug, Clone)] +pub enum Environment { + Local, + Production, +} + impl DatabaseSettings { + #[must_use] pub fn without_db(&self) -> PgConnectOptions { let ssl_mode = if self.require_ssl { PgSslMode::Require @@ -40,6 +55,7 @@ impl DatabaseSettings { .ssl_mode(ssl_mode) } + #[must_use] pub fn with_db(&self) -> PgConnectOptions { self.without_db() .database(&self.database_name) @@ -49,7 +65,7 @@ impl DatabaseSettings { pub fn get_config() -> Result<Settings, config::ConfigError> { let base_path = std::env::current_dir().expect("Failed to determine current directory"); - let config_directory = base_path.join("config"); + let config_directory = base_path.join("backend").join("config"); let env: Environment = std::env::var("APP_ENVIRONMENT") .unwrap_or_else(|_| "local".into()) .try_into() @@ -68,3 +84,26 @@ pub fn get_config() -> Result<Settings, config::ConfigError> { .build()?; settings.try_deserialize::<Settings>() } + +impl Display for Environment { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Self::Local => write!(f, "local"), + Self::Production => write!(f, "production"), + } + } +} + +impl TryFrom<String> for Environment { + type Error = String; + fn try_from(value: String) -> Result<Self, Self::Error> { + match value.to_lowercase().as_str() { + "local" => Ok(Self::Local), + "production" => Ok(Self::Production), + other => Err(format!( + "{other} is not supported environment. \ + Use either `local` or `production`." + )), + } + } +} diff --git a/server/src/db/mod.rs b/backend/src/db/mod.rs similarity index 100% rename from server/src/db/mod.rs rename to backend/src/db/mod.rs diff --git a/server/src/db/users.rs b/backend/src/db/users.rs similarity index 68% rename from server/src/db/users.rs rename to backend/src/db/users.rs index b6cfce6..a2f9b1e 100644 --- a/server/src/db/users.rs +++ b/backend/src/db/users.rs @@ -1,9 +1,17 @@ -use app::models::user::error::UserError; use sqlx::PgPool; +use thiserror::Error; -use crate::{domain::user::NewUser, error::user::ServerUserError}; +use crate::domain::user::{error::UserError, new_user::NewUser}; -#[tracing::instrument(name = "Saving new user details in the database", skip(new_user, pool))] +#[derive(Debug, Error)] +pub enum ServerUserError { + #[error("Database error: {0}")] + Database(#[from] sqlx::Error), + #[error("Database error: {0}")] + User(#[from] UserError), +} + +#[tracing::instrument(name = "Saving new user details in the database", skip(pool, new_user))] pub async fn insert_user(pool: &PgPool, new_user: &NewUser) -> Result<(), ServerUserError> { sqlx::query!( r#" @@ -19,7 +27,7 @@ pub async fn insert_user(pool: &PgPool, new_user: &NewUser) -> Result<(), Server tracing::error!("Failed to execute query: {:?}", e); match e { sqlx::Error::Database(ref dbe) if dbe.constraint() == Some("user_username_key") => { - ServerUserError::User(UserError::UsernameTaken(new_user.username.as_ref().into())) + ServerUserError::User(UserError::UsernameTaken(new_user.username.to_string())) } _ => ServerUserError::Database(e), } diff --git a/backend/src/domain/mod.rs b/backend/src/domain/mod.rs new file mode 100644 index 0000000..22d12a3 --- /dev/null +++ b/backend/src/domain/mod.rs @@ -0,0 +1 @@ +pub mod user; diff --git a/app/src/models/user/error.rs b/backend/src/domain/user/error.rs similarity index 100% rename from app/src/models/user/error.rs rename to backend/src/domain/user/error.rs diff --git a/backend/src/domain/user/mod.rs b/backend/src/domain/user/mod.rs new file mode 100644 index 0000000..6268c80 --- /dev/null +++ b/backend/src/domain/user/mod.rs @@ -0,0 +1,4 @@ +pub mod error; +pub mod new_user; +mod user_code; +mod username; diff --git a/backend/src/domain/user/new_user.rs b/backend/src/domain/user/new_user.rs new file mode 100644 index 0000000..fe70c35 --- /dev/null +++ b/backend/src/domain/user/new_user.rs @@ -0,0 +1,7 @@ +use super::{user_code::UserCode, username::Username}; + +#[derive(Debug, Clone, Default)] +pub struct NewUser { + pub username: Username, + pub code: UserCode, +} diff --git a/server/src/domain/user_code.rs b/backend/src/domain/user/user_code.rs similarity index 97% rename from server/src/domain/user_code.rs rename to backend/src/domain/user/user_code.rs index 25c4ec0..ce35f46 100644 --- a/server/src/domain/user_code.rs +++ b/backend/src/domain/user/user_code.rs @@ -1,4 +1,3 @@ -use app::models::user::error::UserError; use argon2::Argon2; use password_hash::SaltString; use std::ops::Deref; @@ -6,6 +5,8 @@ use std::ops::Deref; use rand::{rngs::OsRng, thread_rng, Rng}; use secrecy::{ExposeSecret, SecretString}; +use super::error::UserError; + #[derive(Debug, Clone)] pub struct UserCode(SecretString); diff --git a/server/src/domain/username.rs b/backend/src/domain/user/username.rs similarity index 70% rename from server/src/domain/username.rs rename to backend/src/domain/user/username.rs index be79867..c62cc07 100644 --- a/server/src/domain/username.rs +++ b/backend/src/domain/user/username.rs @@ -1,12 +1,14 @@ use rand::{seq::SliceRandom, thread_rng, Rng}; -use std::str::FromStr; +use std::{fmt::Display, str::FromStr}; use unicode_segmentation::UnicodeSegmentation; +use super::error::UserError; + #[derive(Debug, Clone)] pub struct Username(String); impl TryFrom<String> for Username { - type Error = String; + type Error = UserError; fn try_from(value: String) -> Result<Self, Self::Error> { let is_empty_or_whitespace = value.trim().is_empty(); let is_too_long = value.graphemes(true).count() > 256; @@ -14,7 +16,7 @@ impl TryFrom<String> for Username { let contains_forbidden_characters = value.chars().any(|c| forbidden_characters.contains(&c)); if is_empty_or_whitespace || is_too_long || contains_forbidden_characters { - return Err(format!("{} is not a valid subscriber name.", value)); + return Err(UserError::UsernameValidation(value)); } Ok(Self(value)) } @@ -34,19 +36,19 @@ impl Default for Username { let mut rng = thread_rng(); - let adjective = adjectives.choose(&mut rng).unwrap(); - let noun = nouns.choose(&mut rng).unwrap(); + let adjective = adjectives.choose(&mut rng).unwrap_or(&"swift"); + let noun = nouns.choose(&mut rng).unwrap_or(&"wolf"); let number = rng.gen_range(100..1000); - let username = format!("{}_{}_{}", adjective, noun, number); + let username = format!("{adjective}_{noun}_{number}"); Self(username) } } impl FromStr for Username { - type Err = String; + type Err = UserError; fn from_str(s: &str) -> Result<Self, Self::Err> { Self::try_from(s.to_owned()) } @@ -57,3 +59,15 @@ impl AsRef<str> for Username { &self.0 } } + +impl Display for Username { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{}", self.0) + } +} + +impl From<Username> for String { + fn from(value: Username) -> Self { + value.0 + } +} diff --git a/backend/src/error/app.rs b/backend/src/error/app.rs new file mode 100644 index 0000000..6a3eca2 --- /dev/null +++ b/backend/src/error/app.rs @@ -0,0 +1,88 @@ +use axum::{http::StatusCode, response::IntoResponse, Json}; +use serde::Serialize; +use thiserror::Error; + +#[derive(Debug, Error)] +pub enum AppError { + // Authentication/Authorization errors + #[error("Unauthorized")] + Unauthorized, + + #[error("Forbidden")] + Forbidden, + + // Validation errors + #[error("Validation error: {0}")] + Validation(String), + + // Resource errors + #[error("Resource not found: {0}")] + NotFound(String), + + #[error("{resource} already exists: {id}")] + AlreadyExists { resource: &'static str, id: String }, + + // Database errors + #[error("Database error")] + Database(#[from] sqlx::Error), + + // Internal errors + #[error("Internal server error")] + Internal(#[from] anyhow::Error), +} +#[derive(Debug, Serialize)] +pub struct ErrorResponse { + pub error: String, + #[serde(skip_serializing_if = "Option::is_none")] + pub details: Option<String>, +} + +impl IntoResponse for AppError { + fn into_response(self) -> axum::response::Response { + let (status, error_message, details) = match self { + // Auth errors + Self::Unauthorized => (StatusCode::UNAUTHORIZED, "Unauthorized".to_string(), None), + Self::Forbidden => (StatusCode::FORBIDDEN, "Forbidden".to_string(), None), + // Validation errors + Self::Validation(msg) => ( + StatusCode::BAD_REQUEST, + "Validation error".to_string(), + Some(msg), + ), + // Resource errors + Self::NotFound(resource) => ( + StatusCode::NOT_FOUND, + "Resource not found".to_string(), + Some(resource), + ), + Self::AlreadyExists { resource, id } => ( + StatusCode::CONFLICT, + format!("{resource} already exists"), + Some(id), + ), + // Database/Internal errors + Self::Database(e) => { + tracing::error!("Database error: {:?}", e); + ( + StatusCode::INTERNAL_SERVER_ERROR, + "Internal server error".to_string(), + None, + ) + } + Self::Internal(e) => { + tracing::error!("Internal error: {:?}", e); + ( + StatusCode::INTERNAL_SERVER_ERROR, + "Internal server error".to_string(), + None, + ) + } + }; + + let body = Json(ErrorResponse { + error: error_message, + details, + }); + (status, body).into_response() + } +} diff --git a/backend/src/error/mod.rs b/backend/src/error/mod.rs new file mode 100644 index 0000000..309be62 --- /dev/null +++ b/backend/src/error/mod.rs @@ -0,0 +1 @@ +pub mod app; diff --git a/backend/src/lib.rs b/backend/src/lib.rs new file mode 100644 index 0000000..6749d30 --- /dev/null +++ b/backend/src/lib.rs @@ -0,0 +1,8 @@ +pub mod config; +pub mod db; +pub mod domain; +pub mod error; +pub mod routes; +pub mod server; +pub mod startup; +pub mod telemetry; diff --git a/server/src/main.rs b/backend/src/main.rs similarity index 57% rename from server/src/main.rs rename to backend/src/main.rs index 78bfd2c..acf5e7f 100644 --- a/server/src/main.rs +++ b/backend/src/main.rs @@ -1,25 +1,17 @@ -mod config; -mod db; -pub mod domain; -mod error; -mod routes; -mod server; -mod startup; - -use app::telemetry::{get_subscriber, init_subscriber}; -use config::get_config; -use error::ServerError; -use leptos::prelude::*; -use server::Server; +use backend::{ + config::get_config, + server::Server, + telemetry::{get_subscriber, init_subscriber}, +}; #[tokio::main] -async fn main() -> Result<(), ServerError> { +async fn main() -> Result<(), std::io::Error> { // Generate the list of routes in your Leptos App let subscriber = get_subscriber("echoes-of-ascension-server", "info", std::io::stdout); init_subscriber(subscriber); let config = get_config().expect("Failed to read configuation."); - + // let application = Server::build(config).await?; application.run_until_stopped().await?; Ok(()) diff --git a/server/src/routes/api/mod.rs b/backend/src/routes/api/mod.rs similarity index 100% rename from server/src/routes/api/mod.rs rename to backend/src/routes/api/mod.rs diff --git a/backend/src/routes/api/v1/auth.rs b/backend/src/routes/api/v1/auth.rs new file mode 100644 index 0000000..43c492f --- /dev/null +++ b/backend/src/routes/api/v1/auth.rs @@ -0,0 +1,72 @@ +use anyhow::anyhow; +use axum::{extract::State, http::StatusCode, response::IntoResponse, Json}; +use secrecy::ExposeSecret; +use serde::{Deserialize, Serialize}; + +use crate::{ + db::users::{insert_user, ServerUserError}, + domain::user::{error::UserError, new_user::NewUser}, + error::app::AppError, + startup::AppState, +}; + +#[derive(Debug, Deserialize)] +pub struct FormData { + pub username: String, +} + +#[derive(Debug, Serialize)] +pub struct Response { + pub username: String, + pub code: String, +} + +#[tracing::instrument( + name = "Creating new user", + skip(state, payload), + fields( + username= %payload.username, + ) +)] +pub async fn register( + State(state): State<AppState>, + Json(payload): Json<FormData>, +) -> Result<impl IntoResponse, impl IntoResponse> { + let new_user = payload + .try_into() + .map_err(|e: UserError| AppError::Validation(e.to_string()))?; + + match insert_user(&state.pool, &new_user).await { + Ok(()) => Ok((StatusCode::CREATED, Json(Response::from(new_user)))), + Err(ServerUserError::User(UserError::UsernameTaken(username))) => { + Err(AppError::AlreadyExists { + resource: "User", + id: username, + }) + } + Err(e) => { + tracing::error!("Failed to register user: {}", e); + Err(AppError::Internal(anyhow!(e))) + } + } +} + +impl TryFrom<FormData> for NewUser { + type Error = UserError; + fn try_from(value: FormData) -> Result<Self, Self::Error> { + let username = value.username.try_into()?; + Ok(Self { + username, + ..Default::default() + }) + } +} + +impl From<NewUser> for Response { + fn from(value: NewUser) -> Self { + Self { + username: value.username.into(), + code: value.code.expose_secret().into(), + } + } +} diff --git a/server/src/routes/api/v1/mod.rs b/backend/src/routes/api/v1/mod.rs similarity index 100% rename from server/src/routes/api/v1/mod.rs rename to backend/src/routes/api/v1/mod.rs diff --git a/server/src/routes/health_check.rs b/backend/src/routes/health_check.rs similarity index 100% rename from server/src/routes/health_check.rs rename to backend/src/routes/health_check.rs diff --git a/server/src/routes/mod.rs b/backend/src/routes/mod.rs similarity index 72% rename from server/src/routes/mod.rs rename to backend/src/routes/mod.rs index 24d8673..c3e1bf7 100644 --- a/server/src/routes/mod.rs +++ b/backend/src/routes/mod.rs @@ -1,7 +1,6 @@ mod api; mod health_check; -use app::{shell, App}; use axum::{ body::Bytes, extract::MatchedPath, @@ -11,7 +10,6 @@ use axum::{ Router, }; use health_check::health_check; -use leptos_axum::{generate_route_list, LeptosRoutes}; use std::time::Duration; use tower_http::{classify::ServerErrorsFailureClass, trace::TraceLayer}; use tracing::{info_span, Span}; @@ -20,24 +18,10 @@ use uuid::Uuid; use crate::startup::AppState; pub fn route(state: AppState) -> Router { - let leptos_options = state.leptos_options.clone(); - let routes = generate_route_list(App); - - let api_router = api::routes().with_state(state.clone()); - Router::new() .route("/health_check", get(health_check)) - // API routes with proper nesting - .nest("/api", api_router) - // Leptos setup - .leptos_routes(&leptos_options, routes, { - { - let leptos_options = leptos_options.clone(); - move || shell(leptos_options.clone()) - } - }) - .fallback(leptos_axum::file_and_error_handler(shell)) - .with_state(leptos_options) + .nest("/api", api::routes()) + .with_state(state) // Tracing layer .layer( TraceLayer::new_for_http() diff --git a/server/src/server.rs b/backend/src/server.rs similarity index 51% rename from server/src/server.rs rename to backend/src/server.rs index 64acdd9..00068ca 100644 --- a/server/src/server.rs +++ b/backend/src/server.rs @@ -1,38 +1,38 @@ -use leptos::prelude::*; use tokio::{net::TcpListener, task::JoinHandle}; use crate::{ config::Settings, - error::ServerError, routes::route, startup::{get_connection_pool, App}, }; #[derive(Debug)] -pub struct Server(JoinHandle<Result<(), std::io::Error>>); +pub struct Server { + port: u16, + server: JoinHandle<Result<(), std::io::Error>>, +} impl Server { - pub async fn build(config: Settings) -> Result<Self, ServerError> { + pub async fn build(config: Settings) -> Result<Self, std::io::Error> { let pool = get_connection_pool(&config.database); - // Get Leptos configuration but override the address - let conf = get_configuration(None)?; - // Use application's address configuration - let addr = conf.leptos_options.site_addr; + let addr = format!("{}:{}", config.application.host, config.application.port); let listener = TcpListener::bind(addr).await?; - - let app_state = App { - pool, - leptos_options: conf.leptos_options, - } - .into(); + let port = listener.local_addr()?.port(); + let app_state = App { pool }.into(); let server = tokio::spawn(async move { axum::serve(listener, route(app_state)).await }); - Ok(Self(server)) + Ok(Self { port, server }) + } + + #[must_use] + #[inline] + pub const fn port(&self) -> u16 { + self.port } pub async fn run_until_stopped(self) -> Result<(), std::io::Error> { - self.0.await? + self.server.await? } } diff --git a/server/src/startup.rs b/backend/src/startup.rs similarity index 81% rename from server/src/startup.rs rename to backend/src/startup.rs index 61e4c1a..a11cc9f 100644 --- a/server/src/startup.rs +++ b/backend/src/startup.rs @@ -1,4 +1,3 @@ -use leptos::config::LeptosOptions; use sqlx::{postgres::PgPoolOptions, PgPool}; use std::sync::Arc; @@ -9,9 +8,10 @@ pub type AppState = Arc<App>; #[derive(Debug)] pub struct App { pub pool: PgPool, - pub leptos_options: LeptosOptions, } +#[must_use] +#[inline] pub fn get_connection_pool(config: &DatabaseSettings) -> PgPool { PgPoolOptions::new().connect_lazy_with(config.with_db()) } diff --git a/app/src/telemetry.rs b/backend/src/telemetry.rs similarity index 92% rename from app/src/telemetry.rs rename to backend/src/telemetry.rs index 8824780..4ce1b7d 100644 --- a/app/src/telemetry.rs +++ b/backend/src/telemetry.rs @@ -4,8 +4,8 @@ use tracing_log::LogTracer; use tracing_subscriber::{fmt::MakeWriter, layer::SubscriberExt, EnvFilter, Registry}; pub fn get_subscriber<Sink>( - name: impl Into<String>, - env_filter: impl Into<EnvFilter>, + name: &str, + env_filter: &str, sink: Sink, ) -> impl Subscriber + Sync + Send where diff --git a/end2end/playwright.config.ts b/end2end/playwright.config.ts deleted file mode 100644 index f38e201..0000000 --- a/end2end/playwright.config.ts +++ /dev/null @@ -1,104 +0,0 @@ -import { defineConfig, devices } from "@playwright/test"; - -/** - * Read environment variables from file. - * https://github.com/motdotla/dotenv - */ -// require('dotenv').config(); - -/** - * See https://playwright.dev/docs/test-configuration. - */ -export default defineConfig({ - testDir: "./tests", - /* Maximum time one test can run for. */ - timeout: 30 * 1000, - expect: { - /** - * Maximum time expect() should wait for the condition to be met. - * For example in `await expect(locator).toHaveText();` - */ - timeout: 5000, - }, - /* Run tests in files in parallel */ - fullyParallel: true, - /* Fail the build on CI if you accidentally left test.only in the source code. */ - forbidOnly: !!process.env.CI, - /* Retry on CI only */ - retries: process.env.CI ? 2 : 0, - /* Opt out of parallel tests on CI. */ - workers: process.env.CI ? 1 : undefined, - /* Reporter to use. See https://playwright.dev/docs/test-reporters */ - reporter: "html", - /* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */ - use: { - /* Maximum time each action such as `click()` can take. Defaults to 0 (no limit). */ - actionTimeout: 0, - /* Base URL to use in actions like `await page.goto('/')`. */ - // baseURL: 'http://localhost:3000', - - /* Collect trace when retrying the failed test. See https://playwright.dev/docs/trace-viewer */ - trace: "on-first-retry", - }, - - /* Configure projects for major browsers */ - projects: [ - { - name: "chromium", - use: { - ...devices["Desktop Chrome"], - }, - }, - - { - name: "firefox", - use: { - ...devices["Desktop Firefox"], - }, - }, - - { - name: "webkit", - use: { - ...devices["Desktop Safari"], - }, - }, - - /* Test against mobile viewports. */ - // { - // name: 'Mobile Chrome', - // use: { - // ...devices['Pixel 5'], - // }, - // }, - // { - // name: 'Mobile Safari', - // use: { - // ...devices['iPhone 12'], - // }, - // }, - - /* Test against branded browsers. */ - // { - // name: 'Microsoft Edge', - // use: { - // channel: 'msedge', - // }, - // }, - // { - // name: 'Google Chrome', - // use: { - // channel: 'chrome', - // }, - // }, - ], - - /* Folder for test artifacts such as screenshots, videos, traces, etc. */ - // outputDir: 'test-results/', - - /* Run your local dev server before starting the tests */ - // webServer: { - // command: 'npm run start', - // port: 3000, - // }, -}); diff --git a/end2end/tests/example.spec.ts b/end2end/tests/example.spec.ts deleted file mode 100644 index 94140aa..0000000 --- a/end2end/tests/example.spec.ts +++ /dev/null @@ -1,15 +0,0 @@ -import { expect, test } from "@playwright/test"; - -test("homepage has title and links to intro page", async ({ page }) => { - await page.goto("http://localhost:3000/"); - - await expect(page).toHaveTitle("Welcome to Leptos"); - - await expect(page.locator("h1")).toHaveText("Welcome to Leptos!"); - - await expect(page.getByRole("button")).toHaveText("Click Me: 0"); - - await page.click("button"); - - await expect(page.getByRole("button")).toHaveText("Click Me: 1"); -}); diff --git a/end2end/tsconfig.json b/end2end/tsconfig.json deleted file mode 100644 index ff6acda..0000000 --- a/end2end/tsconfig.json +++ /dev/null @@ -1,101 +0,0 @@ -{ - "compilerOptions": { - /* Visit https://aka.ms/tsconfig to read more about this file */ - /* Projects */ - // "incremental": true, /* Save .tsbuildinfo files to allow for incremental compilation of projects. */ - // "composite": true, /* Enable constraints that allow a TypeScript project to be used with project references. */ - // "tsBuildInfoFile": "./.tsbuildinfo", /* Specify the path to .tsbuildinfo incremental compilation file. */ - // "disableSourceOfProjectReferenceRedirect": true, /* Disable preferring source files instead of declaration files when referencing composite projects. */ - // "disableSolutionSearching": true, /* Opt a project out of multi-project reference checking when editing. */ - // "disableReferencedProjectLoad": true, /* Reduce the number of projects loaded automatically by TypeScript. */ - /* Language and Environment */ - "target": "es2016", /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */ - // "lib": [], /* Specify a set of bundled library declaration files that describe the target runtime environment. */ - // "jsx": "preserve", /* Specify what JSX code is generated. */ - // "experimentalDecorators": true, /* Enable experimental support for legacy experimental decorators. */ - // "emitDecoratorMetadata": true, /* Emit design-type metadata for decorated declarations in source files. */ - // "jsxFactory": "", /* Specify the JSX factory function used when targeting React JSX emit, e.g. 'React.createElement' or 'h'. */ - // "jsxFragmentFactory": "", /* Specify the JSX Fragment reference used for fragments when targeting React JSX emit e.g. 'React.Fragment' or 'Fragment'. */ - // "jsxImportSource": "", /* Specify module specifier used to import the JSX factory functions when using 'jsx: react-jsx*'. */ - // "reactNamespace": "", /* Specify the object invoked for 'createElement'. This only applies when targeting 'react' JSX emit. */ - // "noLib": true, /* Disable including any library files, including the default lib.d.ts. */ - // "useDefineForClassFields": true, /* Emit ECMAScript-standard-compliant class fields. */ - // "moduleDetection": "auto", /* Control what method is used to detect module-format JS files. */ - /* Modules */ - "module": "commonjs", /* Specify what module code is generated. */ - // "rootDir": "./", /* Specify the root folder within your source files. */ - // "moduleResolution": "node10", /* Specify how TypeScript looks up a file from a given module specifier. */ - // "baseUrl": "./", /* Specify the base directory to resolve non-relative module names. */ - // "paths": {}, /* Specify a set of entries that re-map imports to additional lookup locations. */ - // "rootDirs": [], /* Allow multiple folders to be treated as one when resolving modules. */ - // "typeRoots": [], /* Specify multiple folders that act like './node_modules/@types'. */ - // "types": [], /* Specify type package names to be included without being referenced in a source file. */ - // "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */ - // "moduleSuffixes": [], /* List of file name suffixes to search when resolving a module. */ - // "allowImportingTsExtensions": true, /* Allow imports to include TypeScript file extensions. Requires '--moduleResolution bundler' and either '--noEmit' or '--emitDeclarationOnly' to be set. */ - // "resolvePackageJsonExports": true, /* Use the package.json 'exports' field when resolving package imports. */ - // "resolvePackageJsonImports": true, /* Use the package.json 'imports' field when resolving imports. */ - // "customConditions": [], /* Conditions to set in addition to the resolver-specific defaults when resolving imports. */ - // "resolveJsonModule": true, /* Enable importing .json files. */ - // "allowArbitraryExtensions": true, /* Enable importing files with any extension, provided a declaration file is present. */ - // "noResolve": true, /* Disallow 'import's, 'require's or '<reference>'s from expanding the number of files TypeScript should add to a project. */ - /* JavaScript Support */ - // "allowJs": true, /* Allow JavaScript files to be a part of your program. Use the 'checkJS' option to get errors from these files. */ - // "checkJs": true, /* Enable error reporting in type-checked JavaScript files. */ - // "maxNodeModuleJsDepth": 1, /* Specify the maximum folder depth used for checking JavaScript files from 'node_modules'. Only applicable with 'allowJs'. */ - /* Emit */ - // "declaration": true, /* Generate .d.ts files from TypeScript and JavaScript files in your project. */ - // "declarationMap": true, /* Create sourcemaps for d.ts files. */ - // "emitDeclarationOnly": true, /* Only output d.ts files and not JavaScript files. */ - // "sourceMap": true, /* Create source map files for emitted JavaScript files. */ - // "inlineSourceMap": true, /* Include sourcemap files inside the emitted JavaScript. */ - // "outFile": "./", /* Specify a file that bundles all outputs into one JavaScript file. If 'declaration' is true, also designates a file that bundles all .d.ts output. */ - // "outDir": "./", /* Specify an output folder for all emitted files. */ - // "removeComments": true, /* Disable emitting comments. */ - // "noEmit": true, /* Disable emitting files from a compilation. */ - // "importHelpers": true, /* Allow importing helper functions from tslib once per project, instead of including them per-file. */ - // "importsNotUsedAsValues": "remove", /* Specify emit/checking behavior for imports that are only used for types. */ - // "downlevelIteration": true, /* Emit more compliant, but verbose and less performant JavaScript for iteration. */ - // "sourceRoot": "", /* Specify the root path for debuggers to find the reference source code. */ - // "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */ - // "inlineSources": true, /* Include source code in the sourcemaps inside the emitted JavaScript. */ - // "emitBOM": true, /* Emit a UTF-8 Byte Order Mark (BOM) in the beginning of output files. */ - // "newLine": "crlf", /* Set the newline character for emitting files. */ - // "stripInternal": true, /* Disable emitting declarations that have '@internal' in their JSDoc comments. */ - // "noEmitHelpers": true, /* Disable generating custom helper functions like '__extends' in compiled output. */ - // "noEmitOnError": true, /* Disable emitting files if any type checking errors are reported. */ - // "preserveConstEnums": true, /* Disable erasing 'const enum' declarations in generated code. */ - // "declarationDir": "./", /* Specify the output directory for generated declaration files. */ - // "preserveValueImports": true, /* Preserve unused imported values in the JavaScript output that would otherwise be removed. */ - /* Interop Constraints */ - // "isolatedModules": true, /* Ensure that each file can be safely transpiled without relying on other imports. */ - // "verbatimModuleSyntax": true, /* Do not transform or elide any imports or exports not marked as type-only, ensuring they are written in the output file's format based on the 'module' setting. */ - // "allowSyntheticDefaultImports": true, /* Allow 'import x from y' when a module doesn't have a default export. */ - "esModuleInterop": true, /* Emit additional JavaScript to ease support for importing CommonJS modules. This enables 'allowSyntheticDefaultImports' for type compatibility. */ - // "preserveSymlinks": true, /* Disable resolving symlinks to their realpath. This correlates to the same flag in node. */ - "forceConsistentCasingInFileNames": true, /* Ensure that casing is correct in imports. */ - /* Type Checking */ - "strict": true, /* Enable all strict type-checking options. */ - // "noImplicitAny": true, /* Enable error reporting for expressions and declarations with an implied 'any' type. */ - // "strictNullChecks": true, /* When type checking, take into account 'null' and 'undefined'. */ - // "strictFunctionTypes": true, /* When assigning functions, check to ensure parameters and the return values are subtype-compatible. */ - // "strictBindCallApply": true, /* Check that the arguments for 'bind', 'call', and 'apply' methods match the original function. */ - // "strictPropertyInitialization": true, /* Check for class properties that are declared but not set in the constructor. */ - // "noImplicitThis": true, /* Enable error reporting when 'this' is given the type 'any'. */ - // "useUnknownInCatchVariables": true, /* Default catch clause variables as 'unknown' instead of 'any'. */ - // "alwaysStrict": true, /* Ensure 'use strict' is always emitted. */ - // "noUnusedLocals": true, /* Enable error reporting when local variables aren't read. */ - // "noUnusedParameters": true, /* Raise an error when a function parameter isn't read. */ - // "exactOptionalPropertyTypes": true, /* Interpret optional property types as written, rather than adding 'undefined'. */ - // "noImplicitReturns": true, /* Enable error reporting for codepaths that do not explicitly return in a function. */ - // "noFallthroughCasesInSwitch": true, /* Enable error reporting for fallthrough cases in switch statements. */ - // "noUncheckedIndexedAccess": true, /* Add 'undefined' to a type when accessed using an index. */ - // "noImplicitOverride": true, /* Ensure overriding members in derived classes are marked with an override modifier. */ - // "noPropertyAccessFromIndexSignature": true, /* Enforces using indexed accessors for keys declared using an indexed type. */ - // "allowUnusedLabels": true, /* Disable error reporting for unused labels. */ - // "allowUnreachableCode": true, /* Disable error reporting for unreachable code. */ - /* Completeness */ - // "skipDefaultLibCheck": true, /* Skip type checking .d.ts files that are included with TypeScript. */ - "skipLibCheck": true /* Skip type checking all .d.ts files. */ - } -} diff --git a/frontend/.gitignore b/frontend/.gitignore new file mode 100644 index 0000000..6985cf1 --- /dev/null +++ b/frontend/.gitignore @@ -0,0 +1,14 @@ +# Generated by Cargo +# will have compiled files and executables +debug/ +target/ + +# Remove Cargo.lock from gitignore if creating an executable, leave it for libraries +# More information here https://doc.rust-lang.org/cargo/guide/cargo-toml-vs-cargo-lock.html +Cargo.lock + +# These are backup files generated by rustfmt +**/*.rs.bk + +# MSVC Windows builds of rustc generate these, which store debugging information +*.pdb diff --git a/frontend/Cargo.toml b/frontend/Cargo.toml index af0df23..b530614 100644 --- a/frontend/Cargo.toml +++ b/frontend/Cargo.toml @@ -6,14 +6,98 @@ edition = "2021" [lib] crate-type = ["cdylib", "rlib"] -# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html - [dependencies] -app = { path = "../app", default-features = false, features = ["hydrate"] } -leptos = { workspace = true, features = ["hydrate"] } +leptos = { version = "0.7.0", features = ["nightly"] } +leptos_router = { version = "0.7.0", features = ["nightly"] } +axum = { version = "0.7", optional = true } +console_error_panic_hook = { version = "0.1", optional = true } +leptos_axum = { version = "0.7.0", optional = true } +leptos_meta = { version = "0.7.0" } +tokio = { workspace = true, features = ["rt-multi-thread"], optional = true } +wasm-bindgen = { version = "=0.2.100", optional = true } +reqwest = { version = "0.12", features = ["json"] } +serde.workspace = true -tracing.workspace = true -wasm-bindgen.workspace = true +[features] +hydrate = ["leptos/hydrate", "dep:console_error_panic_hook", "dep:wasm-bindgen"] +ssr = [ + "dep:axum", + "dep:tokio", + "dep:leptos_axum", + "leptos/ssr", + "leptos_meta/ssr", + "leptos_router/ssr", +] + +# Defines a size-optimized profile for the WASM bundle in release mode +[profile.wasm-release] +inherits = "release" +opt-level = "z" +lto = true +codegen-units = 1 +panic = "abort" + +[package.metadata.leptos] +# The name used by wasm-bindgen/cargo-leptos for the JS/WASM bundle. Defaults to the crate name +output-name = "frontend" + +# The site root folder is where cargo-leptos generate all output. WARNING: all content of this folder will be erased on a rebuild. Use it in your server setup. +site-root = "target/site" + +# The site-root relative folder where all compiled output (JS, WASM and CSS) is written +# Defaults to pkg +site-pkg-dir = "pkg" + +# [Optional] The source CSS file. If it ends with .sass or .scss then it will be compiled by dart-sass into CSS. The CSS is optimized by Lightning CSS before being written to <site-root>/<site-pkg>/app.css +style-file = "style/main.scss" +# Assets source dir. All files found here will be copied and synchronized to site-root. +# The assets-dir cannot have a sub directory with the same name/path as site-pkg-dir. +# +# Optional. Env: LEPTOS_ASSETS_DIR. +assets-dir = "public" + +# The IP and port (ex: 127.0.0.1:3000) where the server serves the content. Use it in your server setup. +site-addr = "127.0.0.1:3000" + +# The port to use for automatic reload monitoring +reload-port = 3001 + +# [Optional] Command to use when running end2end tests. It will run in the end2end dir. +# [Windows] for non-WSL use "npx.cmd playwright test" +# This binary name can be checked in Powershell with Get-Command npx +end2end-cmd = "npx playwright test" +end2end-dir = "end2end" + +# The browserlist query used for optimizing the CSS. +browserquery = "defaults" + +# The environment Leptos will run in, usually either "DEV" or "PROD" +env = "DEV" + +# The features to use when compiling the bin target +# +# Optional. Can be over-ridden with the command line parameter --bin-features +bin-features = ["ssr"] + +# If the --no-default-features flag should be used when compiling the bin target +# +# Optional. Defaults to false. +bin-default-features = false + +# The features to use when compiling the lib target +# +# Optional. Can be over-ridden with the command line parameter --lib-features +lib-features = ["hydrate"] + +# If the --no-default-features flag should be used when compiling the lib target +# +# Optional. Defaults to false. +lib-default-features = false + +# The profile to use for the lib target when compiling for release +# +# Optional. Defaults to "release". +lib-profile-release = "wasm-release" [lints] workspace = true diff --git a/README.md b/frontend/README.md similarity index 55% rename from README.md rename to frontend/README.md index a7a8569..15ad4f4 100644 --- a/README.md +++ b/frontend/README.md @@ -5,7 +5,7 @@ # Leptos Axum Starter Template -This is a template for use with the [Leptos](https://github.com/leptos-rs/leptos) web framework and the [cargo-leptos](https://github.com/leptos-rs/cargo-leptos) tool using [Axum](https://github.com/tokio-rs/axum). +This is a template for use with the [Leptos](https://github.com/leptos-rs/leptos) web framework and the [cargo-leptos](https://github.com/akesson/cargo-leptos) tool using [Axum](https://github.com/tokio-rs/axum). ## Creating your template repo @@ -16,31 +16,19 @@ cargo install cargo-leptos --locked ``` Then run - ```bash -cargo leptos new --git https://github.com/leptos-rs/start-axum-workspace-0.7/ +cargo leptos new --git https://github.com/leptos-rs/start-axum-0.7 ``` to generate a new project template. ```bash -cd {projectname} +cd frontend ``` to go to your newly created project. Feel free to explore the project structure, but the best place to start with your application code is in `src/app.rs`. -Additionally, Cargo.toml may need updating as new versions of the dependencies are released, especially if things are not working after a `cargo update`. - -### Islands support - -Note that for islands to work correctly, you need to have a `use app;` in your frontend `lib.rs` otherwise rustc / wasm_bindgen gets confused. -To prevent clippy from complaining, at the top of the `frontend/lib.rs` file place: - -```rust -#[allow(clippy::single_component_path_imports)] -#[allow(unused_imports)] -use app; -``` +Addtionally, Cargo.toml may need updating as new versions of the dependencies are released, especially if things are not working after a `cargo update`. ## Running your project @@ -53,13 +41,12 @@ cargo leptos watch By default, `cargo-leptos` uses `nightly` Rust, `cargo-generate`, and `sass`. If you run into any trouble, you may need to install one or more of these tools. 1. `rustup toolchain install nightly --allow-downgrade` - make sure you have Rust nightly -2. `rustup default nightly` - setup nightly as default, or you can use rust-toolchain file later on -3. `rustup target add wasm32-unknown-unknown` - add the ability to compile Rust to WebAssembly -4. `cargo install cargo-generate` - install `cargo-generate` binary (should be installed automatically in future) -5. `npm install -g sass` - install `dart-sass` (should be optional in future +2. `rustup target add wasm32-unknown-unknown` - add the ability to compile Rust to WebAssembly +3. `cargo install cargo-generate` - install `cargo-generate` binary (should be installed automatically in future) +4. `npm install -g sass` - install `dart-sass` (should be optional in future +5. Run `npm install` in end2end subdirectory before test ## Compiling for Release - ```bash cargo leptos build --release ``` @@ -67,52 +54,36 @@ cargo leptos build --release Will generate your server binary in target/server/release and your site package in target/site ## Testing Your Project - -Cargo-leptos uses (<https://playwright.dev)[Playwright>] as the end-to-end test tool. - -Prior to the first run of the end-to-end tests run Playwright must be installed. -In the project's `end2end` directory run `npm install -D playwright @playwright/test` to install playwright and browser specific APIs. - -To run the tests during development in the project root run: - ```bash cargo leptos end-to-end ``` -To run tests for release in the project root run: - ```bash cargo leptos end-to-end --release ``` -There are some examples tests are located in `end2end/tests` directory that pass tests with the sample Leptos app. - -A web-based report on tests is available by running `npx playwright show-report` in the `end2end` directory. +Cargo-leptos uses Playwright as the end-to-end test tool. +Tests are located in end2end/tests directory. ## Executing a Server on a Remote Machine Without the Toolchain - After running a `cargo leptos build --release` the minimum files needed are: 1. The server binary located in `target/server/release` 2. The `site` directory and all files within located in `target/site` Copy these files to your remote server. The directory structure should be: - ```text -start-axum-workspace +frontend site/ ``` - Set the following environment variables (updating for your project as needed): - -```text -LEPTOS_OUTPUT_NAME="start-axum-workspace" -LEPTOS_SITE_ROOT="site" -LEPTOS_SITE_PKG_DIR="pkg" -LEPTOS_SITE_ADDR="127.0.0.1:3000" -LEPTOS_RELOAD_PORT="3001" +```sh +export LEPTOS_OUTPUT_NAME="frontend" +export LEPTOS_SITE_ROOT="site" +export LEPTOS_SITE_PKG_DIR="pkg" +export LEPTOS_SITE_ADDR="127.0.0.1:3000" +export LEPTOS_RELOAD_PORT="3001" ``` - Finally, run the server binary. ## Licensing diff --git a/frontend/end2end/.gitignore b/frontend/end2end/.gitignore new file mode 100644 index 0000000..169a2af --- /dev/null +++ b/frontend/end2end/.gitignore @@ -0,0 +1,3 @@ +node_modules +playwright-report +test-results diff --git a/end2end/package-lock.json b/frontend/end2end/package-lock.json similarity index 100% rename from end2end/package-lock.json rename to frontend/end2end/package-lock.json diff --git a/end2end/package.json b/frontend/end2end/package.json similarity index 100% rename from end2end/package.json rename to frontend/end2end/package.json diff --git a/frontend/end2end/playwright.config.ts b/frontend/end2end/playwright.config.ts new file mode 100644 index 0000000..aee2d46 --- /dev/null +++ b/frontend/end2end/playwright.config.ts @@ -0,0 +1,105 @@ +import type { PlaywrightTestConfig } from "@playwright/test"; +import { devices, defineConfig } from "@playwright/test"; + +/** + * Read environment variables from file. + * https://github.com/motdotla/dotenv + */ +// require('dotenv').config(); + +/** + * See https://playwright.dev/docs/test-configuration. + */ +export default defineConfig({ + testDir: "./tests", + /* Maximum time one test can run for. */ + timeout: 30 * 1000, + expect: { + /** + * Maximum time expect() should wait for the condition to be met. + * For example in `await expect(locator).toHaveText();` + */ + timeout: 5000, + }, + /* Run tests in files in parallel */ + fullyParallel: true, + /* Fail the build on CI if you accidentally left test.only in the source code. */ + forbidOnly: !!process.env.CI, + /* Retry on CI only */ + retries: process.env.CI ? 2 : 0, + /* Opt out of parallel tests on CI. */ + workers: process.env.CI ? 1 : undefined, + /* Reporter to use. See https://playwright.dev/docs/test-reporters */ + reporter: "html", + /* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */ + use: { + /* Maximum time each action such as `click()` can take. Defaults to 0 (no limit). */ + actionTimeout: 0, + /* Base URL to use in actions like `await page.goto('/')`. */ + // baseURL: 'http://localhost:3000', + + /* Collect trace when retrying the failed test. See https://playwright.dev/docs/trace-viewer */ + trace: "on-first-retry", + }, + + /* Configure projects for major browsers */ + projects: [ + { + name: "chromium", + use: { + ...devices["Desktop Chrome"], + }, + }, + + { + name: "firefox", + use: { + ...devices["Desktop Firefox"], + }, + }, + + { + name: "webkit", + use: { + ...devices["Desktop Safari"], + }, + }, + + /* Test against mobile viewports. */ + // { + // name: 'Mobile Chrome', + // use: { + // ...devices['Pixel 5'], + // }, + // }, + // { + // name: 'Mobile Safari', + // use: { + // ...devices['iPhone 12'], + // }, + // }, + + /* Test against branded browsers. */ + // { + // name: 'Microsoft Edge', + // use: { + // channel: 'msedge', + // }, + // }, + // { + // name: 'Google Chrome', + // use: { + // channel: 'chrome', + // }, + // }, + ], + + /* Folder for test artifacts such as screenshots, videos, traces, etc. */ + // outputDir: 'test-results/', + + /* Run your local dev server before starting the tests */ + // webServer: { + // command: 'npm run start', + // port: 3000, + // }, +}); diff --git a/frontend/end2end/tests/example.spec.ts b/frontend/end2end/tests/example.spec.ts new file mode 100644 index 0000000..0139fc3 --- /dev/null +++ b/frontend/end2end/tests/example.spec.ts @@ -0,0 +1,9 @@ +import { test, expect } from "@playwright/test"; + +test("homepage has title and heading text", async ({ page }) => { + await page.goto("http://localhost:3000/"); + + await expect(page).toHaveTitle("Welcome to Leptos"); + + await expect(page.locator("h1")).toHaveText("Welcome to Leptos!"); +}); diff --git a/frontend/end2end/tsconfig.json b/frontend/end2end/tsconfig.json new file mode 100644 index 0000000..e075f97 --- /dev/null +++ b/frontend/end2end/tsconfig.json @@ -0,0 +1,109 @@ +{ + "compilerOptions": { + /* Visit https://aka.ms/tsconfig to read more about this file */ + + /* Projects */ + // "incremental": true, /* Save .tsbuildinfo files to allow for incremental compilation of projects. */ + // "composite": true, /* Enable constraints that allow a TypeScript project to be used with project references. */ + // "tsBuildInfoFile": "./.tsbuildinfo", /* Specify the path to .tsbuildinfo incremental compilation file. */ + // "disableSourceOfProjectReferenceRedirect": true, /* Disable preferring source files instead of declaration files when referencing composite projects. */ + // "disableSolutionSearching": true, /* Opt a project out of multi-project reference checking when editing. */ + // "disableReferencedProjectLoad": true, /* Reduce the number of projects loaded automatically by TypeScript. */ + + /* Language and Environment */ + "target": "es2016", /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */ + // "lib": [], /* Specify a set of bundled library declaration files that describe the target runtime environment. */ + // "jsx": "preserve", /* Specify what JSX code is generated. */ + // "experimentalDecorators": true, /* Enable experimental support for legacy experimental decorators. */ + // "emitDecoratorMetadata": true, /* Emit design-type metadata for decorated declarations in source files. */ + // "jsxFactory": "", /* Specify the JSX factory function used when targeting React JSX emit, e.g. 'React.createElement' or 'h'. */ + // "jsxFragmentFactory": "", /* Specify the JSX Fragment reference used for fragments when targeting React JSX emit e.g. 'React.Fragment' or 'Fragment'. */ + // "jsxImportSource": "", /* Specify module specifier used to import the JSX factory functions when using 'jsx: react-jsx*'. */ + // "reactNamespace": "", /* Specify the object invoked for 'createElement'. This only applies when targeting 'react' JSX emit. */ + // "noLib": true, /* Disable including any library files, including the default lib.d.ts. */ + // "useDefineForClassFields": true, /* Emit ECMAScript-standard-compliant class fields. */ + // "moduleDetection": "auto", /* Control what method is used to detect module-format JS files. */ + + /* Modules */ + "module": "commonjs", /* Specify what module code is generated. */ + // "rootDir": "./", /* Specify the root folder within your source files. */ + // "moduleResolution": "node10", /* Specify how TypeScript looks up a file from a given module specifier. */ + // "baseUrl": "./", /* Specify the base directory to resolve non-relative module names. */ + // "paths": {}, /* Specify a set of entries that re-map imports to additional lookup locations. */ + // "rootDirs": [], /* Allow multiple folders to be treated as one when resolving modules. */ + // "typeRoots": [], /* Specify multiple folders that act like './node_modules/@types'. */ + // "types": [], /* Specify type package names to be included without being referenced in a source file. */ + // "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */ + // "moduleSuffixes": [], /* List of file name suffixes to search when resolving a module. */ + // "allowImportingTsExtensions": true, /* Allow imports to include TypeScript file extensions. Requires '--moduleResolution bundler' and either '--noEmit' or '--emitDeclarationOnly' to be set. */ + // "resolvePackageJsonExports": true, /* Use the package.json 'exports' field when resolving package imports. */ + // "resolvePackageJsonImports": true, /* Use the package.json 'imports' field when resolving imports. */ + // "customConditions": [], /* Conditions to set in addition to the resolver-specific defaults when resolving imports. */ + // "resolveJsonModule": true, /* Enable importing .json files. */ + // "allowArbitraryExtensions": true, /* Enable importing files with any extension, provided a declaration file is present. */ + // "noResolve": true, /* Disallow 'import's, 'require's or '<reference>'s from expanding the number of files TypeScript should add to a project. */ + + /* JavaScript Support */ + // "allowJs": true, /* Allow JavaScript files to be a part of your program. Use the 'checkJS' option to get errors from these files. */ + // "checkJs": true, /* Enable error reporting in type-checked JavaScript files. */ + // "maxNodeModuleJsDepth": 1, /* Specify the maximum folder depth used for checking JavaScript files from 'node_modules'. Only applicable with 'allowJs'. */ + + /* Emit */ + // "declaration": true, /* Generate .d.ts files from TypeScript and JavaScript files in your project. */ + // "declarationMap": true, /* Create sourcemaps for d.ts files. */ + // "emitDeclarationOnly": true, /* Only output d.ts files and not JavaScript files. */ + // "sourceMap": true, /* Create source map files for emitted JavaScript files. */ + // "inlineSourceMap": true, /* Include sourcemap files inside the emitted JavaScript. */ + // "outFile": "./", /* Specify a file that bundles all outputs into one JavaScript file. If 'declaration' is true, also designates a file that bundles all .d.ts output. */ + // "outDir": "./", /* Specify an output folder for all emitted files. */ + // "removeComments": true, /* Disable emitting comments. */ + // "noEmit": true, /* Disable emitting files from a compilation. */ + // "importHelpers": true, /* Allow importing helper functions from tslib once per project, instead of including them per-file. */ + // "importsNotUsedAsValues": "remove", /* Specify emit/checking behavior for imports that are only used for types. */ + // "downlevelIteration": true, /* Emit more compliant, but verbose and less performant JavaScript for iteration. */ + // "sourceRoot": "", /* Specify the root path for debuggers to find the reference source code. */ + // "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */ + // "inlineSources": true, /* Include source code in the sourcemaps inside the emitted JavaScript. */ + // "emitBOM": true, /* Emit a UTF-8 Byte Order Mark (BOM) in the beginning of output files. */ + // "newLine": "crlf", /* Set the newline character for emitting files. */ + // "stripInternal": true, /* Disable emitting declarations that have '@internal' in their JSDoc comments. */ + // "noEmitHelpers": true, /* Disable generating custom helper functions like '__extends' in compiled output. */ + // "noEmitOnError": true, /* Disable emitting files if any type checking errors are reported. */ + // "preserveConstEnums": true, /* Disable erasing 'const enum' declarations in generated code. */ + // "declarationDir": "./", /* Specify the output directory for generated declaration files. */ + // "preserveValueImports": true, /* Preserve unused imported values in the JavaScript output that would otherwise be removed. */ + + /* Interop Constraints */ + // "isolatedModules": true, /* Ensure that each file can be safely transpiled without relying on other imports. */ + // "verbatimModuleSyntax": true, /* Do not transform or elide any imports or exports not marked as type-only, ensuring they are written in the output file's format based on the 'module' setting. */ + // "allowSyntheticDefaultImports": true, /* Allow 'import x from y' when a module doesn't have a default export. */ + "esModuleInterop": true, /* Emit additional JavaScript to ease support for importing CommonJS modules. This enables 'allowSyntheticDefaultImports' for type compatibility. */ + // "preserveSymlinks": true, /* Disable resolving symlinks to their realpath. This correlates to the same flag in node. */ + "forceConsistentCasingInFileNames": true, /* Ensure that casing is correct in imports. */ + + /* Type Checking */ + "strict": true, /* Enable all strict type-checking options. */ + // "noImplicitAny": true, /* Enable error reporting for expressions and declarations with an implied 'any' type. */ + // "strictNullChecks": true, /* When type checking, take into account 'null' and 'undefined'. */ + // "strictFunctionTypes": true, /* When assigning functions, check to ensure parameters and the return values are subtype-compatible. */ + // "strictBindCallApply": true, /* Check that the arguments for 'bind', 'call', and 'apply' methods match the original function. */ + // "strictPropertyInitialization": true, /* Check for class properties that are declared but not set in the constructor. */ + // "noImplicitThis": true, /* Enable error reporting when 'this' is given the type 'any'. */ + // "useUnknownInCatchVariables": true, /* Default catch clause variables as 'unknown' instead of 'any'. */ + // "alwaysStrict": true, /* Ensure 'use strict' is always emitted. */ + // "noUnusedLocals": true, /* Enable error reporting when local variables aren't read. */ + // "noUnusedParameters": true, /* Raise an error when a function parameter isn't read. */ + // "exactOptionalPropertyTypes": true, /* Interpret optional property types as written, rather than adding 'undefined'. */ + // "noImplicitReturns": true, /* Enable error reporting for codepaths that do not explicitly return in a function. */ + // "noFallthroughCasesInSwitch": true, /* Enable error reporting for fallthrough cases in switch statements. */ + // "noUncheckedIndexedAccess": true, /* Add 'undefined' to a type when accessed using an index. */ + // "noImplicitOverride": true, /* Ensure overriding members in derived classes are marked with an override modifier. */ + // "noPropertyAccessFromIndexSignature": true, /* Enforces using indexed accessors for keys declared using an indexed type. */ + // "allowUnusedLabels": true, /* Disable error reporting for unused labels. */ + // "allowUnreachableCode": true, /* Disable error reporting for unreachable code. */ + + /* Completeness */ + // "skipDefaultLibCheck": true, /* Skip type checking .d.ts files that are included with TypeScript. */ + "skipLibCheck": true /* Skip type checking all .d.ts files. */ + } +} diff --git a/public/favicon.ico b/frontend/public/favicon.ico similarity index 100% rename from public/favicon.ico rename to frontend/public/favicon.ico diff --git a/frontend/src/app.rs b/frontend/src/app.rs new file mode 100644 index 0000000..7c31dc8 --- /dev/null +++ b/frontend/src/app.rs @@ -0,0 +1,61 @@ +use leptos::prelude::*; +use leptos_meta::{provide_meta_context, MetaTags, Stylesheet, Title}; +use leptos_router::{ + components::{Route, Router, Routes}, + StaticSegment, +}; + +pub fn shell(options: LeptosOptions) -> impl IntoView { + view! { + <!DOCTYPE html> + <html lang="en"> + <head> + <meta charset="utf-8" /> + <meta name="viewport" content="width=device-width, initial-scale=1" /> + <AutoReload options=options.clone() /> + <HydrationScripts options /> + <MetaTags /> + </head> + <body> + <App /> + </body> + </html> + } +} + +#[component] +pub fn App() -> impl IntoView { + // Provides context that manages stylesheets, titles, meta tags, etc. + provide_meta_context(); + + view! { + // injects a stylesheet into the document <head> + // id=leptos means cargo-leptos will hot-reload this stylesheet + <Stylesheet id="leptos" href="/pkg/frontend.css" /> + + // sets the document title + <Title text="Welcome to Leptos" /> + + // content for this welcome page + <Router> + <main> + <Routes fallback=|| "Page not found.".into_view()> + <Route path=StaticSegment("") view=HomePage /> + </Routes> + </main> + </Router> + } +} + +/// Renders the home page of your application. +#[component] +fn HomePage() -> impl IntoView { + // Creates a reactive value to update the button + let count = RwSignal::new(0); + let on_click = move |_| *count.write() += 1; + + view! { + <h1>"Welcome to Leptos!"</h1> + <button on:click=on_click>"Click Me: " {count}</button> + } +} diff --git a/frontend/src/lib.rs b/frontend/src/lib.rs index 2f01a33..151489d 100644 --- a/frontend/src/lib.rs +++ b/frontend/src/lib.rs @@ -1,11 +1,9 @@ -use app::telemetry::{get_subscriber, init_subscriber}; +pub mod app; +#[cfg(feature = "hydrate")] #[wasm_bindgen::prelude::wasm_bindgen] pub fn hydrate() { - use app::*; - // initializes logging using the `log` crate - let subscriber = get_subscriber("echoes-of-ascension-frontend", "info", std::io::stdout); - init_subscriber(subscriber); - + use crate::app::*; + console_error_panic_hook::set_once(); leptos::mount::hydrate_body(App); } diff --git a/frontend/src/main.rs b/frontend/src/main.rs new file mode 100644 index 0000000..e8a999d --- /dev/null +++ b/frontend/src/main.rs @@ -0,0 +1,38 @@ +#[cfg(feature = "ssr")] +#[tokio::main] +async fn main() { + use axum::Router; + use frontend::app::*; + use leptos::logging::log; + use leptos::prelude::*; + use leptos_axum::{generate_route_list, LeptosRoutes}; + + let conf = get_configuration(None).unwrap(); + let addr = conf.leptos_options.site_addr; + let leptos_options = conf.leptos_options; + // Generate the list of routes in your Leptos App + let routes = generate_route_list(App); + + let app = Router::new() + .leptos_routes(&leptos_options, routes, { + let leptos_options = leptos_options.clone(); + move || shell(leptos_options.clone()) + }) + .fallback(leptos_axum::file_and_error_handler(shell)) + .with_state(leptos_options); + + // run our app with hyper + // `axum::Server` is a re-export of `hyper::Server` + log!("listening on http://{}", &addr); + let listener = tokio::net::TcpListener::bind(&addr).await.unwrap(); + axum::serve(listener, app.into_make_service()) + .await + .unwrap(); +} + +#[cfg(not(feature = "ssr"))] +pub fn main() { + // no client-side main function + // unless we want this to work with e.g., Trunk for pure client-side testing + // see lib.rs for hydration function instead +} diff --git a/style/main.scss b/frontend/style/main.scss similarity index 100% rename from style/main.scss rename to frontend/style/main.scss diff --git a/justfile b/justfile index 8305c08..b81e594 100644 --- a/justfile +++ b/justfile @@ -18,7 +18,10 @@ setup: # Start development server with hot reload dev: kill-server db-migrate - cargo leptos watch | bunyan + #!/usr/bin/env bash + (RUSTC_WRAPPER=sccache cargo watch -x "run -p backend" | bunyan) & \ + (RUSTC_WRAPPER=sccache cargo leptos watch | bunyan) & \ + wait # Run cargo check on both native and wasm targets check: @@ -49,6 +52,7 @@ clean: # Build for development build-dev: + cargo build cargo leptos build # Build for production @@ -79,7 +83,6 @@ kill-server: pkill -f "target/debug/server" || true pkill -f "cargo-leptos" || true - # Database Commands # Setup the database @@ -90,7 +93,7 @@ alias migrate:=db-migrate alias m:=db-migrate # Migrate db-migrate: - sqlx migrate run + sqlx migrate run --source backend/migrations # Generate sqlx prepare check files db-prepare: diff --git a/server/Cargo.toml b/server/Cargo.toml deleted file mode 100644 index f96557f..0000000 --- a/server/Cargo.toml +++ /dev/null @@ -1,41 +0,0 @@ -[package] -name = "server" -version = "0.1.0" -edition = "2021" - -# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html - -[dependencies] -app = { path = "../app", default-features = false, features = ["ssr"] } -leptos = { workspace = true, features = ["ssr"] } -leptos_axum.workspace = true -thiserror.workspace = true - -axum.workspace = true -tokio = { workspace = true, features = ["rt-multi-thread"] } -sqlx = { version = "0.8", features = [ - "runtime-tokio", - "macros", - "postgres", - "uuid", - "chrono", - "migrate", -] } -secrecy.workspace = true -serde.workspace = true -serde-aux.workspace = true -config.workspace = true -tower.workspace = true -tower-http.workspace = true -tracing.workspace = true -tracing-log.workspace = true -uuid.workspace = true -rand.workspace = true -argon2.workspace = true -unicode-segmentation.workspace = true -password-hash.workspace = true -hex.workspace = true -leptos_router.workspace = true - -[lints] -workspace = true diff --git a/server/src/domain/mod.rs b/server/src/domain/mod.rs deleted file mode 100644 index 548fd5c..0000000 --- a/server/src/domain/mod.rs +++ /dev/null @@ -1,3 +0,0 @@ -pub mod user; -mod user_code; -mod username; diff --git a/server/src/domain/user.rs b/server/src/domain/user.rs deleted file mode 100644 index b9d9a60..0000000 --- a/server/src/domain/user.rs +++ /dev/null @@ -1,30 +0,0 @@ -use app::models::{response::RegisterResponse, user::form::RegisterUserForm}; -use secrecy::ExposeSecret; - -use super::{user_code::UserCode, username::Username}; - -#[derive(Debug, Clone, Default)] -pub struct NewUser { - pub username: Username, - pub code: UserCode, -} - -impl TryFrom<RegisterUserForm> for NewUser { - type Error = String; - fn try_from(value: RegisterUserForm) -> Result<Self, Self::Error> { - let username = value.username.try_into()?; - Ok(Self { - username, - ..Default::default() - }) - } -} - -impl From<NewUser> for RegisterResponse { - fn from(value: NewUser) -> Self { - Self { - username: value.username.as_ref().into(), - code: value.code.expose_secret().into(), - } - } -} diff --git a/server/src/error/mod.rs b/server/src/error/mod.rs deleted file mode 100644 index cdf12d2..0000000 --- a/server/src/error/mod.rs +++ /dev/null @@ -1,4 +0,0 @@ -pub mod server; -pub mod user; - -pub use server::ServerError; diff --git a/server/src/error/server.rs b/server/src/error/server.rs deleted file mode 100644 index 5337966..0000000 --- a/server/src/error/server.rs +++ /dev/null @@ -1,15 +0,0 @@ -use leptos::config::errors::LeptosConfigError; -use sqlx; -use thiserror::Error; - -#[derive(Debug, Error)] -pub enum ServerError { - #[error("IO error: {0}")] - Io(#[from] std::io::Error), - - #[error("Leptos configuration error: {0}")] - LeptosConfig(#[from] LeptosConfigError), - - #[error("Database error: {0}")] - Database(#[from] sqlx::Error), -} diff --git a/server/src/error/user.rs b/server/src/error/user.rs deleted file mode 100644 index 3522187..0000000 --- a/server/src/error/user.rs +++ /dev/null @@ -1,21 +0,0 @@ -use app::models::user::error::UserError; -use sqlx; -use thiserror::Error; - -#[derive(Debug, Error)] -pub enum ServerUserError { - #[error("Database error: {0}")] - Database(#[from] sqlx::Error), - - #[error("{0}")] - User(#[from] UserError), -} - -impl From<ServerUserError> for UserError { - fn from(error: ServerUserError) -> Self { - match error { - ServerUserError::Database(e) => Self::Internal(e.to_string()), - ServerUserError::User(e) => e, - } - } -} diff --git a/server/src/fileserv.rs b/server/src/fileserv.rs deleted file mode 100644 index e2020c2..0000000 --- a/server/src/fileserv.rs +++ /dev/null @@ -1,44 +0,0 @@ -use app::App; -use axum::response::Response as AxumResponse; -use axum::{ - body::Body, - extract::State, - http::{Request, Response, StatusCode, Uri}, - response::IntoResponse, -}; -use leptos::prelude::*; -use tower::ServiceExt; -use tower_http::services::ServeDir; - -pub async fn file_and_error_handler( - uri: Uri, - State(options): State<LeptosOptions>, - req: Request<Body>, -) -> AxumResponse { - let root = options.site_root.clone(); - let res = get_static_file(uri.clone(), &root).await.unwrap(); - - if res.status() == StatusCode::OK { - res.into_response() - } else { - let handler = - leptos_axum::render_app_to_stream(options.to_owned(), move || view! { <App /> }); - handler(req).await.into_response() - } -} - -async fn get_static_file(uri: Uri, root: &str) -> Result<Response<Body>, (StatusCode, String)> { - let req = Request::builder() - .uri(uri.clone()) - .body(Body::empty()) - .unwrap(); - // `ServeDir` implements `tower::Service` so we can call it with `tower::ServiceExt::oneshot` - // This path is relative to the cargo root - match ServeDir::new(root).oneshot(req).await { - Ok(res) => Ok(res.map(Body::new)), - Err(err) => Err(( - StatusCode::INTERNAL_SERVER_ERROR, - format!("Something went wrong: {err}"), - )), - } -} diff --git a/server/src/routes/api/v1/auth.rs b/server/src/routes/api/v1/auth.rs deleted file mode 100644 index 5ba825f..0000000 --- a/server/src/routes/api/v1/auth.rs +++ /dev/null @@ -1,41 +0,0 @@ -use app::models::{ - response::{ErrorResponse, RegisterResponse}, - user::{error::UserError, form::RegisterUserForm}, -}; -use axum::{extract::State, http::StatusCode, response::IntoResponse, Json}; - -use crate::{db::users::insert_user, error::user::ServerUserError, startup::AppState}; - -#[tracing::instrument( - name = "Creating new user", - skip(payload, state), - fields( - username= %payload.username, - ) -)] -pub async fn register( - State(state): State<AppState>, - Json(payload): Json<RegisterUserForm>, -) -> Result<impl IntoResponse, impl IntoResponse> { - let new_user = payload - .try_into() - .map_err(|e| (StatusCode::BAD_REQUEST, Json(ErrorResponse::from(e))))?; - - match insert_user(&state.pool, &new_user).await { - Ok(_) => Ok((StatusCode::CREATED, Json(RegisterResponse::from(new_user)))), - Err(ServerUserError::User(UserError::UsernameTaken(username))) => Err(( - StatusCode::CONFLICT, - Json(ErrorResponse::from(format!( - "Username {} is already taken.", - username - ))), - )), - Err(e) => { - tracing::error!("Failed to register user: {}", e); - Err(( - StatusCode::INTERNAL_SERVER_ERROR, - Json(ErrorResponse::from("Internal server error")), - )) - } - } -}