From a13526a77adfff212b458ffe717d56ce9af80ee0 Mon Sep 17 00:00:00 2001 From: Norbert Melzer Date: Fri, 5 Jun 2026 23:52:02 +0200 Subject: [PATCH] very basic user/auth --- .env | 7 + .envrc | 2 + docker-compose.yml | 21 + nix/shells/rust.nix | 2 + rust/.envrc | 2 + rust/Cargo.lock | 681 +++++++++++++++++- rust/Cargo.toml | 8 +- rust/diesel.toml | 10 + rust/henka-api/Cargo.toml | 6 + .../migrations/.diesel_lock} | 0 rust/henka-api/migrations/.keep | 0 .../down.sql | 8 + .../up.sql | 38 + .../down.sql | 2 + .../up.sql | 6 + rust/henka-api/src/config.rs | 1 - rust/henka-api/src/context.rs | 21 +- rust/henka-api/src/diesel_schema.rs | 13 + rust/henka-api/src/errors.rs | 19 + rust/henka-api/src/lib.rs | 5 +- rust/henka-api/src/main.rs | 8 +- rust/henka-api/src/middleware.rs | 46 +- rust/henka-api/src/model.rs | 1 + rust/henka-api/src/model/user.rs | 20 + rust/henka-api/src/routes.rs | 42 +- rust/henka-api/src/schema.rs | 83 ++- rust/henka-api/src/schema/me.rs | 8 + rust/henka-api/tests/graphql.rs | 79 +- 28 files changed, 1083 insertions(+), 56 deletions(-) create mode 100644 .env create mode 100644 docker-compose.yml create mode 100644 rust/diesel.toml rename rust/{f7c1a2d347e4c52d5fb8d10cb4d94b5884e546fb.tar.gz => henka-api/migrations/.diesel_lock} (100%) create mode 100644 rust/henka-api/migrations/.keep create mode 100644 rust/henka-api/migrations/00000000000000_diesel_initial_setup/down.sql create mode 100644 rust/henka-api/migrations/00000000000000_diesel_initial_setup/up.sql create mode 100644 rust/henka-api/migrations/2026-06-05-215200-0000_create_users/down.sql create mode 100644 rust/henka-api/migrations/2026-06-05-215200-0000_create_users/up.sql delete mode 100644 rust/henka-api/src/config.rs create mode 100644 rust/henka-api/src/diesel_schema.rs create mode 100644 rust/henka-api/src/errors.rs create mode 100644 rust/henka-api/src/model.rs create mode 100644 rust/henka-api/src/model/user.rs create mode 100644 rust/henka-api/src/schema/me.rs diff --git a/.env b/.env new file mode 100644 index 0000000..33244bd --- /dev/null +++ b/.env @@ -0,0 +1,7 @@ +POSTGRES_USER=henka +POSTGRES_PASSWORD=henka_dev_password +POSTGRES_DB=henka +POSTGRES_HOST=localhost +POSTGRES_PORT=5433 +DATABASE_URL=postgres://henka:henka_dev_password@localhost:5433/henka +TEST_DATABASE_URL=postgres://henka:henka_dev_password@localhost:5433/henka_test diff --git a/.envrc b/.envrc index 76e01c1..b91318c 100644 --- a/.envrc +++ b/.envrc @@ -1,3 +1,5 @@ watch_file $(find . -name '*.nix') +dotenv_if_exists .env + use nix diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..42225c0 --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,21 @@ +services: + postgres: + image: postgres:18-alpine + container_name: henka-db + restart: unless-stopped + ports: + - "5433:5432" + environment: + POSTGRES_USER: henka + POSTGRES_PASSWORD: henka_dev_password + POSTGRES_DB: henka + volumes: + - pgdata:/var/lib/postgresql + healthcheck: + test: ["CMD-SHELL", "pg_isready -U ${POSTGRES_USER} -d ${POSTGRES_DB}"] + interval: 10s + timeout: 5s + retries: 5 + +volumes: + pgdata: diff --git a/nix/shells/rust.nix b/nix/shells/rust.nix index bedc76f..79e2dd9 100644 --- a/nix/shells/rust.nix +++ b/nix/shells/rust.nix @@ -10,6 +10,7 @@ bacon, cargo-workspaces, diesel-cli, + postgresql_18, rust-bin, defaultShell, }: let @@ -25,6 +26,7 @@ bacon cargo-workspaces diesel-cli + postgresql_18.lib rust rustfmt ]; diff --git a/rust/.envrc b/rust/.envrc index c3df4c6..c739342 100644 --- a/rust/.envrc +++ b/rust/.envrc @@ -1,3 +1,5 @@ watch_file $(find .. -name '*.nix') +dotenv_if_exists ../.env + use nix diff --git a/rust/Cargo.lock b/rust/Cargo.lock index de33f3d..0345749 100644 --- a/rust/Cargo.lock +++ b/rust/Cargo.lock @@ -17,6 +17,21 @@ version = "2.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa" +[[package]] +name = "aho-corasick" +version = "1.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ddd31a130427c27518df266943a5308ed92d4b226cc639f5a8f1002816174301" +dependencies = [ + "memchr", +] + +[[package]] +name = "anyhow" +version = "1.0.102" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f202df86484c868dbad7eaa557ef785d5c66295e41b460ef922eca0723b842c" + [[package]] name = "arcstr" version = "1.2.0" @@ -126,6 +141,12 @@ version = "0.22.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" +[[package]] +name = "bitflags" +version = "2.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "843867be96c8daad0d758b57df9392b6d8d271134fce549de6ce169ff98a92af" + [[package]] name = "block-buffer" version = "0.10.4" @@ -135,6 +156,18 @@ dependencies = [ "generic-array", ] +[[package]] +name = "bumpalo" +version = "3.20.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d20789868f4b01b2f2caec9f5c4e0213b41e3e5702a50157d699ae31ced2fcb" + +[[package]] +name = "byteorder" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" + [[package]] name = "bytes" version = "1.11.1" @@ -225,12 +258,88 @@ dependencies = [ "typenum", ] +[[package]] +name = "darling" +version = "0.21.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9cdf337090841a411e2a7f3deb9187445851f91b309c0c0a29e05f74a00a48c0" +dependencies = [ + "darling_core", + "darling_macro", +] + +[[package]] +name = "darling_core" +version = "0.21.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1247195ecd7e3c85f83c8d2a366e4210d588e802133e1e355180a9870b517ea4" +dependencies = [ + "fnv", + "ident_case", + "proc-macro2", + "quote", + "strsim", + "syn", +] + +[[package]] +name = "darling_macro" +version = "0.21.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d38308df82d1080de0afee5d069fa14b0326a88c14f15c5ccda35b4a6c414c81" +dependencies = [ + "darling_core", + "quote", + "syn", +] + [[package]] name = "data-encoding" version = "2.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a4ae5f15dda3c708c0ade84bfee31ccab44a3da4f88015ed22f63732abe300c8" +[[package]] +name = "deadpool" +version = "0.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0be2b1d1d6ec8d846f05e137292d0b89133caf95ef33695424c09568bdd39b1b" +dependencies = [ + "deadpool-runtime", + "lazy_static", + "num_cpus", + "tokio", +] + +[[package]] +name = "deadpool-diesel" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "590573e9e29c5190a5ff782136f871e6e652e35d598a349888e028693601adf1" +dependencies = [ + "deadpool", + "deadpool-sync", + "diesel", +] + +[[package]] +name = "deadpool-runtime" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "092966b41edc516079bdf31ec78a2e0588d1d0c08f78b91d8307215928642b2b" +dependencies = [ + "tokio", +] + +[[package]] +name = "deadpool-sync" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "524bc3df0d57e98ecd022e21ba31166c2625e7d3e5bcc4510efaeeab4abcab04" +dependencies = [ + "deadpool-runtime", +] + [[package]] name = "derive_more" version = "2.1.1" @@ -265,6 +374,54 @@ dependencies = [ "syn", ] +[[package]] +name = "diesel" +version = "2.3.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29fe29a87fb84c631ffb3ba21798c4b1f3a964701ba78f0dce4bf8668562ec88" +dependencies = [ + "bitflags", + "byteorder", + "diesel_derives", + "downcast-rs", + "itoa", + "pq-sys", + "uuid", +] + +[[package]] +name = "diesel_derives" +version = "2.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d1817b7f4279b947fc4cafddec12b0e5f8727141706561ce3ac94a60bddd1cf5" +dependencies = [ + "diesel_table_macro_syntax", + "dsl_auto_type", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "diesel_migrations" +version = "2.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "28d0f4a98124ba6d4ca75da535f65984badec16a003b6e2f94a01e31a79490b8" +dependencies = [ + "diesel", + "migrations_internals", + "migrations_macros", +] + +[[package]] +name = "diesel_table_macro_syntax" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fe2444076b48641147115697648dc743c2c00b61adade0f01ce67133c7babe8c" +dependencies = [ + "syn", +] + [[package]] name = "digest" version = "0.10.7" @@ -286,6 +443,26 @@ dependencies = [ "syn", ] +[[package]] +name = "downcast-rs" +version = "2.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "117240f60069e65410b3ae1bb213295bd828f707b5bec6596a1afc8793ce0cbc" + +[[package]] +name = "dsl_auto_type" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dd122633e4bef06db27737f21d3738fb89c8f6d5360d6d9d7635dda142a7757e" +dependencies = [ + "darling", + "either", + "heck", + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "either" version = "1.16.0" @@ -314,6 +491,12 @@ version = "1.0.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" +[[package]] +name = "foldhash" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2" + [[package]] name = "form_urlencoded" version = "1.2.2" @@ -394,6 +577,12 @@ version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "037711b3d59c33004d3856fbdc83b99d4ff37a24768fa1be9ce3538a1cde4393" +[[package]] +name = "futures-timer" +version = "3.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "af43fadb8a98512d547e37b4e92e0ced13e205c061b87b4623eff01d918d6968" + [[package]] name = "futures-util" version = "0.3.32" @@ -429,8 +618,21 @@ checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd" dependencies = [ "cfg-if", "libc", - "r-efi", + "r-efi 5.3.0", + "wasip2", +] + +[[package]] +name = "getrandom" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0de51e6874e94e7bf76d726fc5d13ba782deca734ff60d5bb2fb2607c7406555" +dependencies = [ + "cfg-if", + "libc", + "r-efi 6.0.0", "wasip2", + "wasip3", ] [[package]] @@ -439,6 +641,12 @@ version = "0.32.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e629b9b98ef3dd8afe6ca2bd0f89306cec16d43d907889945bc5d6687f2f13c7" +[[package]] +name = "glob" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0cc23270f6e1808e30a928bdc84dea0b9b4136a8bc82338574f23baf47bbd280" + [[package]] name = "h2" version = "0.4.13" @@ -458,12 +666,27 @@ dependencies = [ "tracing", ] +[[package]] +name = "hashbrown" +version = "0.15.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1" +dependencies = [ + "foldhash", +] + [[package]] name = "hashbrown" version = "0.17.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ed5909b6e89a2db4456e54cd5f673791d7eca6732202bbf2a9cc504fe2f9b84a" +[[package]] +name = "heck" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" + [[package]] name = "henka" version = "0.0.1" @@ -474,13 +697,18 @@ version = "0.0.1" dependencies = [ "axum", "color-eyre", + "deadpool-diesel", + "diesel", + "diesel_migrations", "eyre", "futures", "juniper", "juniper_axum", "juniper_graphql_ws", + "rstest", "serde", "serde_json", + "thiserror", "tokio", "tokio-stream", "tokio-tungstenite", @@ -488,12 +716,19 @@ dependencies = [ "tower-service", "tracing", "tracing-subscriber", + "uuid", ] [[package]] name = "henka-service" version = "0.0.1" +[[package]] +name = "hermit-abi" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc0fef456e4baa96da950455cd02c081ca953b141298e41db3fc7e36b1da849c" + [[package]] name = "http" version = "1.4.1" @@ -658,6 +893,18 @@ dependencies = [ "zerovec", ] +[[package]] +name = "id-arena" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d3067d79b975e8844ca9eb072e16b31c3c1c36928edf9c6789548c524d0d954" + +[[package]] +name = "ident_case" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39" + [[package]] name = "idna" version = "1.1.0" @@ -692,7 +939,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d466e9454f08e4a911e14806c24e16fba1b4c121d1ea474396f396069cf949d9" dependencies = [ "equivalent", - "hashbrown", + "hashbrown 0.17.1", "serde", "serde_core", ] @@ -712,6 +959,16 @@ version = "1.0.18" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682" +[[package]] +name = "js-sys" +version = "0.3.95" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2964e92d1d9dc3364cae4d718d93f227e3abb088e747d92e0395bfdedf1c12ca" +dependencies = [ + "once_cell", + "wasm-bindgen", +] + [[package]] name = "juniper" version = "0.17.1" @@ -792,6 +1049,12 @@ version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" +[[package]] +name = "leb128fmt" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09edd9e8b54e49e587e4f6295a7d29c3ea94d469cb40ab8ca70b288248a81db2" + [[package]] name = "libc" version = "0.2.186" @@ -822,6 +1085,27 @@ version = "2.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6b947ae49db0d222b1dbc6b113ce7248a3fc3a6ca21b696717bfc000ba4484d8" +[[package]] +name = "migrations_internals" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "36c791ecdf977c99f45f23280405d7723727470f6689a5e6dbf513ac547ae10d" +dependencies = [ + "serde", + "toml", +] + +[[package]] +name = "migrations_macros" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "36fc5ac76be324cfd2d3f2cf0fdf5d5d3c4f14ed8aaebadb09e304ba42282703" +dependencies = [ + "migrations_internals", + "proc-macro2", + "quote", +] + [[package]] name = "mime" version = "0.3.17" @@ -857,6 +1141,16 @@ dependencies = [ "windows-sys", ] +[[package]] +name = "num_cpus" +version = "1.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91df4bbde75afed763b708b7eee1e8e7651e02d97f6d5dd763e89367e957b23b" +dependencies = [ + "hermit-abi", + "libc", +] + [[package]] name = "object" version = "0.37.3" @@ -896,6 +1190,12 @@ version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" +[[package]] +name = "pkg-config" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7edddbd0b52d732b21ad9a5fab5c704c14cd949e5e9a1ec5929a24fded1b904c" + [[package]] name = "potential_utf" version = "0.1.5" @@ -914,6 +1214,36 @@ dependencies = [ "zerocopy", ] +[[package]] +name = "pq-sys" +version = "0.7.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "574ddd6a267294433f140b02a726b0640c43cf7c6f717084684aaa3b285aba61" +dependencies = [ + "libc", + "pkg-config", + "vcpkg", +] + +[[package]] +name = "prettyplease" +version = "0.2.37" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "479ca8adacdd7ce8f1fb39ce9ecccbfe93a3f1344b3d0d97f20bc0196208f62b" +dependencies = [ + "proc-macro2", + "syn", +] + +[[package]] +name = "proc-macro-crate" +version = "3.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e67ba7e9b2b56446f1d419b1d807906278ffa1a658a8a5d8a39dcb1f5a78614f" +dependencies = [ + "toml_edit", +] + [[package]] name = "proc-macro2" version = "1.0.106" @@ -938,6 +1268,12 @@ version = "5.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" +[[package]] +name = "r-efi" +version = "6.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf" + [[package]] name = "rand" version = "0.9.4" @@ -964,7 +1300,7 @@ version = "0.9.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "76afc826de14238e6e8c374ddcc1fa19e374fd8dd986b0d2af0d02377261d83c" dependencies = [ - "getrandom", + "getrandom 0.3.4", ] [[package]] @@ -987,6 +1323,70 @@ dependencies = [ "syn", ] +[[package]] +name = "regex" +version = "1.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e10754a14b9137dd7b1e3e5b0493cc9171fdd105e0ab477f51b72e7f3ac0e276" +dependencies = [ + "aho-corasick", + "memchr", + "regex-automata", + "regex-syntax", +] + +[[package]] +name = "regex-automata" +version = "0.4.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e1dd4122fc1595e8162618945476892eefca7b88c52820e74af6262213cae8f" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax", +] + +[[package]] +name = "regex-syntax" +version = "0.8.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc897dd8d9e8bd1ed8cdad82b5966c3e0ecae09fb1907d58efaa013543185d0a" + +[[package]] +name = "relative-path" +version = "1.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba39f3699c378cd8970968dcbff9c43159ea4cfbd88d43c00b22f2ef10a435d2" + +[[package]] +name = "rstest" +version = "0.26.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f5a3193c063baaa2a95a33f03035c8a72b83d97a54916055ba22d35ed3839d49" +dependencies = [ + "futures-timer", + "futures-util", + "rstest_macros", +] + +[[package]] +name = "rstest_macros" +version = "0.26.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c845311f0ff7951c5506121a9ad75aec44d083c31583b2ea5a30bcb0b0abba0" +dependencies = [ + "cfg-if", + "glob", + "proc-macro-crate", + "proc-macro2", + "quote", + "regex", + "relative-path", + "rustc_version", + "syn", + "unicode-ident", +] + [[package]] name = "rustc-demangle" version = "0.1.27" @@ -1074,6 +1474,15 @@ dependencies = [ "serde_core", ] +[[package]] +name = "serde_spanned" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6662b5879511e06e8999a8a235d848113e942c9124f211511b16466ee2995f26" +dependencies = [ + "serde_core", +] + [[package]] name = "serde_urlencoded" version = "0.7.1" @@ -1140,6 +1549,12 @@ version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" +[[package]] +name = "strsim" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" + [[package]] name = "syn" version = "2.0.117" @@ -1269,6 +1684,58 @@ dependencies = [ "tokio", ] +[[package]] +name = "toml" +version = "0.9.12+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf92845e79fc2e2def6a5d828f0801e29a2f8acc037becc5ab08595c7d5e9863" +dependencies = [ + "serde_core", + "serde_spanned", + "toml_datetime 0.7.5+spec-1.1.0", + "toml_parser", + "winnow 0.7.15", +] + +[[package]] +name = "toml_datetime" +version = "0.7.5+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92e1cfed4a3038bc5a127e35a2d360f145e1f4b971b551a2ba5fd7aedf7e1347" +dependencies = [ + "serde_core", +] + +[[package]] +name = "toml_datetime" +version = "1.1.1+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3165f65f62e28e0115a00b2ebdd37eb6f3b641855f9d636d3cd4103767159ad7" +dependencies = [ + "serde_core", +] + +[[package]] +name = "toml_edit" +version = "0.25.12+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d2153edc6955a6c354fad8f5efd38b6a8769bdccf9fe50f8e1329f81b0baa5d7" +dependencies = [ + "indexmap", + "toml_datetime 1.1.1+spec-1.1.0", + "toml_parser", + "winnow 1.0.3", +] + +[[package]] +name = "toml_parser" +version = "1.1.2+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2abe9b86193656635d2411dc43050282ca48aa31c2451210f4202550afb7526" +dependencies = [ + "winnow 1.0.3", +] + [[package]] name = "tower" version = "0.5.3" @@ -1423,9 +1890,14 @@ checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" [[package]] name = "uuid" -version = "1.22.0" +version = "1.23.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a68d3c8f01c0cfa54a75291d83601161799e4a89a39e0929f4b0354d88757a37" +checksum = "d258b83ceec21034727ecee8c382cfa6c3e133699b0742c64571814fb420c9f7" +dependencies = [ + "getrandom 0.4.2", + "js-sys", + "wasm-bindgen", +] [[package]] name = "valuable" @@ -1433,6 +1905,12 @@ version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ba73ea9cf16a25df0c8caa16c51acb937d5712a8429db78a3ee29d5dcacd3a65" +[[package]] +name = "vcpkg" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" + [[package]] name = "version_check" version = "0.9.5" @@ -1451,7 +1929,95 @@ version = "1.0.3+wasi-0.2.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "20064672db26d7cdc89c7798c48a0fdfac8213434a1186e5ef29fd560ae223d6" dependencies = [ - "wit-bindgen", + "wit-bindgen 0.57.1", +] + +[[package]] +name = "wasip3" +version = "0.4.0+wasi-0.3.0-rc-2026-01-06" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5428f8bf88ea5ddc08faddef2ac4a67e390b88186c703ce6dbd955e1c145aca5" +dependencies = [ + "wit-bindgen 0.51.0", +] + +[[package]] +name = "wasm-bindgen" +version = "0.2.118" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0bf938a0bacb0469e83c1e148908bd7d5a6010354cf4fb73279b7447422e3a89" +dependencies = [ + "cfg-if", + "once_cell", + "rustversion", + "wasm-bindgen-macro", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.118" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eeff24f84126c0ec2db7a449f0c2ec963c6a49efe0698c4242929da037ca28ed" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.118" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d08065faf983b2b80a79fd87d8254c409281cf7de75fc4b773019824196c904" +dependencies = [ + "bumpalo", + "proc-macro2", + "quote", + "syn", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.118" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5fd04d9e306f1907bd13c6361b5c6bfc7b3b3c095ed3f8a9246390f8dbdee129" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "wasm-encoder" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "990065f2fe63003fe337b932cfb5e3b80e0b4d0f5ff650e6985b1048f62c8319" +dependencies = [ + "leb128fmt", + "wasmparser", +] + +[[package]] +name = "wasm-metadata" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb0e353e6a2fbdc176932bbaab493762eb1255a7900fe0fea1a2f96c296cc909" +dependencies = [ + "anyhow", + "indexmap", + "wasm-encoder", + "wasmparser", +] + +[[package]] +name = "wasmparser" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47b807c72e1bac69382b3a6fb3dbe8ea4c0ed87ff5629b8685ae6b9a611028fe" +dependencies = [ + "bitflags", + "hashbrown 0.15.5", + "indexmap", + "semver", ] [[package]] @@ -1469,12 +2035,115 @@ dependencies = [ "windows-link", ] +[[package]] +name = "winnow" +version = "0.7.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df79d97927682d2fd8adb29682d1140b343be4ac0f08fd68b7765d9c059d3945" + +[[package]] +name = "winnow" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0592e1c9d151f854e6fd382574c3a0855250e1d9b2f99d9281c6e6391af352f1" +dependencies = [ + "memchr", +] + +[[package]] +name = "wit-bindgen" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d7249219f66ced02969388cf2bb044a09756a083d0fab1e566056b04d9fbcaa5" +dependencies = [ + "wit-bindgen-rust-macro", +] + [[package]] name = "wit-bindgen" version = "0.57.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1ebf944e87a7c253233ad6766e082e3cd714b5d03812acc24c318f549614536e" +[[package]] +name = "wit-bindgen-core" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ea61de684c3ea68cb082b7a88508a8b27fcc8b797d738bfc99a82facf1d752dc" +dependencies = [ + "anyhow", + "heck", + "wit-parser", +] + +[[package]] +name = "wit-bindgen-rust" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b7c566e0f4b284dd6561c786d9cb0142da491f46a9fbed79ea69cdad5db17f21" +dependencies = [ + "anyhow", + "heck", + "indexmap", + "prettyplease", + "syn", + "wasm-metadata", + "wit-bindgen-core", + "wit-component", +] + +[[package]] +name = "wit-bindgen-rust-macro" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c0f9bfd77e6a48eccf51359e3ae77140a7f50b1e2ebfe62422d8afdaffab17a" +dependencies = [ + "anyhow", + "prettyplease", + "proc-macro2", + "quote", + "syn", + "wit-bindgen-core", + "wit-bindgen-rust", +] + +[[package]] +name = "wit-component" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d66ea20e9553b30172b5e831994e35fbde2d165325bec84fc43dbf6f4eb9cb2" +dependencies = [ + "anyhow", + "bitflags", + "indexmap", + "log", + "serde", + "serde_derive", + "serde_json", + "wasm-encoder", + "wasm-metadata", + "wasmparser", + "wit-parser", +] + +[[package]] +name = "wit-parser" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ecc8ac4bc1dc3381b7f59c34f00b67e18f910c2c0f50015669dde7def656a736" +dependencies = [ + "anyhow", + "id-arena", + "indexmap", + "log", + "semver", + "serde", + "serde_derive", + "serde_json", + "unicode-xid", + "wasmparser", +] + [[package]] name = "writeable" version = "0.6.3" diff --git a/rust/Cargo.toml b/rust/Cargo.toml index 81b3b2b..3502102 100644 --- a/rust/Cargo.toml +++ b/rust/Cargo.toml @@ -9,19 +9,25 @@ package.authors = ["Norbert Melzer"] [workspace.dependencies] color-eyre = "0.6.5" +deadpool-diesel = { version = "0.6.1", features = ["postgres"] } +diesel = { version = "2.3.10", features = ["postgres", "uuid"] } +diesel_migrations = "2.3.2" eyre = "0.6.12" futures = "0.3" juniper = { version = "0.17", default-features = false, features = ["uuid"] } juniper_axum = { version = "0.3", features = ["subscriptions"] } juniper_graphql_ws = { version = "0.5", features = ["graphql-transport-ws"] } +rstest = "0.26.1" serde = { version = "1.0", features = ["derive"] } serde_json = { version = "1.0" } +thiserror = "2.0.18" tokio-stream = "0.1" tokio-tungstenite = "0.29" tower = { version = "0.5", default-features = false } tower-service = "0.3" tracing = "0.1" tracing-subscriber = "0.3" +uuid = { version = "1.23.2", features = ["v4"] } [workspace.dependencies.axum] version = "0.8" @@ -59,7 +65,7 @@ assertions_on_result_states = "warn" # Don't Do Bad Async Stuff - prevent deadlocks and concurrency bugs await_holding_lock = "warn" await_holding_refcell_ref = "warn" -if_let_mutex = "warn" # only relevant on editions before 2024 +if_let_mutex = "warn" # only relevant on editions before 2024 large_futures = "warn" # Don't Do Unsafe Things with Memory diff --git a/rust/diesel.toml b/rust/diesel.toml new file mode 100644 index 0000000..619b8ad --- /dev/null +++ b/rust/diesel.toml @@ -0,0 +1,10 @@ +# For documentation on how to configure this file, +# see https://diesel.rs/guides/configuring-diesel-cli + +[print_schema] +file = "henka-api/src/diesel_schema.rs" +custom_type_derives = ["diesel::query_builder::QueryId", "Clone"] +import_types = ["diesel::sql_types::*"] + +[migrations_directory] +dir = "henka-api/migrations" diff --git a/rust/henka-api/Cargo.toml b/rust/henka-api/Cargo.toml index ceec15a..baddd67 100644 --- a/rust/henka-api/Cargo.toml +++ b/rust/henka-api/Cargo.toml @@ -8,6 +8,9 @@ authors.workspace = true [dependencies] axum.workspace = true color-eyre.workspace = true +deadpool-diesel.workspace = true +diesel.workspace = true +diesel_migrations.workspace = true eyre.workspace = true futures.workspace = true juniper.workspace = true @@ -15,14 +18,17 @@ juniper_axum.workspace = true juniper_graphql_ws.workspace = true serde.workspace = true serde_json.workspace = true +thiserror.workspace = true tokio-stream.workspace = true tokio-tungstenite.workspace = true tokio.workspace = true tower-service.workspace = true tracing-subscriber.workspace = true tracing.workspace = true +uuid.workspace = true [dev-dependencies] +rstest.workspace = true tower.workspace = true [lints] diff --git a/rust/f7c1a2d347e4c52d5fb8d10cb4d94b5884e546fb.tar.gz b/rust/henka-api/migrations/.diesel_lock similarity index 100% rename from rust/f7c1a2d347e4c52d5fb8d10cb4d94b5884e546fb.tar.gz rename to rust/henka-api/migrations/.diesel_lock diff --git a/rust/henka-api/migrations/.keep b/rust/henka-api/migrations/.keep new file mode 100644 index 0000000..e69de29 diff --git a/rust/henka-api/migrations/00000000000000_diesel_initial_setup/down.sql b/rust/henka-api/migrations/00000000000000_diesel_initial_setup/down.sql new file mode 100644 index 0000000..f350f9b --- /dev/null +++ b/rust/henka-api/migrations/00000000000000_diesel_initial_setup/down.sql @@ -0,0 +1,8 @@ +-- This file was automatically created by Diesel to setup helper functions +-- and other internal bookkeeping. This file is safe to edit, any future +-- changes will be added to existing projects as new migrations. + +DROP FUNCTION IF EXISTS diesel_manage_updated_at(_tbl regclass); +DROP FUNCTION IF EXISTS diesel_set_updated_at(); +DROP EXTENSION IF EXISTS pgcrypto; + diff --git a/rust/henka-api/migrations/00000000000000_diesel_initial_setup/up.sql b/rust/henka-api/migrations/00000000000000_diesel_initial_setup/up.sql new file mode 100644 index 0000000..8d1c08c --- /dev/null +++ b/rust/henka-api/migrations/00000000000000_diesel_initial_setup/up.sql @@ -0,0 +1,38 @@ +-- This file was automatically created by Diesel to setup helper functions +-- and other internal bookkeeping. This file is safe to edit, any future +-- changes will be added to existing projects as new migrations. + + + + +-- Sets up a trigger for the given table to automatically set a column called +-- `updated_at` whenever the row is modified (unless `updated_at` was included +-- in the modified columns) +-- +-- # Example +-- +-- ```sql +-- CREATE TABLE users (id SERIAL PRIMARY KEY, updated_at TIMESTAMP NOT NULL DEFAULT NOW()); +-- +-- SELECT diesel_manage_updated_at('users'); +-- ``` +CREATE OR REPLACE FUNCTION diesel_manage_updated_at(_tbl regclass) RETURNS VOID AS $$ +BEGIN + EXECUTE format('CREATE TRIGGER set_updated_at BEFORE UPDATE ON %s + FOR EACH ROW EXECUTE PROCEDURE diesel_set_updated_at()', _tbl); +END; +$$ LANGUAGE plpgsql; + +CREATE OR REPLACE FUNCTION diesel_set_updated_at() RETURNS trigger AS $$ +BEGIN + IF ( + NEW IS DISTINCT FROM OLD AND + NEW.updated_at IS NOT DISTINCT FROM OLD.updated_at + ) THEN + NEW.updated_at := current_timestamp; + END IF; + RETURN NEW; +END; +$$ LANGUAGE plpgsql; + +CREATE EXTENSION IF NOT EXISTS pgcrypto; diff --git a/rust/henka-api/migrations/2026-06-05-215200-0000_create_users/down.sql b/rust/henka-api/migrations/2026-06-05-215200-0000_create_users/down.sql new file mode 100644 index 0000000..cf7e736 --- /dev/null +++ b/rust/henka-api/migrations/2026-06-05-215200-0000_create_users/down.sql @@ -0,0 +1,2 @@ +-- This file should undo anything in `up.sql` +DROP TABLE users diff --git a/rust/henka-api/migrations/2026-06-05-215200-0000_create_users/up.sql b/rust/henka-api/migrations/2026-06-05-215200-0000_create_users/up.sql new file mode 100644 index 0000000..c7bea41 --- /dev/null +++ b/rust/henka-api/migrations/2026-06-05-215200-0000_create_users/up.sql @@ -0,0 +1,6 @@ +-- Your SQL goes here +CREATE TABLE users ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + nick_name VARCHAR(32) NOT NULL, + password_hash VARCHAR(32) NOT NULL +); diff --git a/rust/henka-api/src/config.rs b/rust/henka-api/src/config.rs deleted file mode 100644 index 9c3bd47..0000000 --- a/rust/henka-api/src/config.rs +++ /dev/null @@ -1 +0,0 @@ -pub const AUTH_TOKEN: &str = "henka-demo-token"; diff --git a/rust/henka-api/src/context.rs b/rust/henka-api/src/context.rs index 4812b02..268b6ad 100644 --- a/rust/henka-api/src/context.rs +++ b/rust/henka-api/src/context.rs @@ -1,6 +1,23 @@ -#[derive(Clone, Copy, Debug, Default)] +use std::sync::{Arc, Mutex}; + +use deadpool_diesel::postgres::Pool; + +use crate::model::user::User; + +#[derive(Clone)] pub(crate) struct Context { - pub authorized: bool, + pub current_user: Arc>>, + pub pool: Arc, } impl juniper::Context for Context {} + +impl std::fmt::Debug for Context { + fn fmt(&self, f: &mut std::fmt::Formatter) -> Result<(), std::fmt::Error> { + write!( + f, + "Context {{ current_user: {:?}, pool: _ }}", + self.current_user + ) + } +} diff --git a/rust/henka-api/src/diesel_schema.rs b/rust/henka-api/src/diesel_schema.rs new file mode 100644 index 0000000..678f1c7 --- /dev/null +++ b/rust/henka-api/src/diesel_schema.rs @@ -0,0 +1,13 @@ +// @generated automatically by Diesel CLI. + +diesel::table! { + use diesel::sql_types::*; + + users (id) { + id -> Uuid, + #[max_length = 32] + nick_name -> Varchar, + #[max_length = 32] + password_hash -> Varchar, + } +} diff --git a/rust/henka-api/src/errors.rs b/rust/henka-api/src/errors.rs new file mode 100644 index 0000000..f1e3402 --- /dev/null +++ b/rust/henka-api/src/errors.rs @@ -0,0 +1,19 @@ +use thiserror::Error; + +#[derive(Error, Debug)] +pub(crate) enum AccountCreationError { + #[error("account creation forbidden when logged in")] + ForbiddenWhenLoggedIn, + #[error("email and password confirmations do not match")] + ConfirmationsDontMatch, + #[error("email confirmation does not match")] + EmailConfirmationDoesntMatch, + #[error("password confirmation does not match")] + PasswordConfirmationDoesntMatch, +} + +#[derive(Error, Debug)] +pub(crate) enum AccessDeniedError { + #[error("no user session")] + NoUserSession, +} diff --git a/rust/henka-api/src/lib.rs b/rust/henka-api/src/lib.rs index 73834c6..be1ff0d 100644 --- a/rust/henka-api/src/lib.rs +++ b/rust/henka-api/src/lib.rs @@ -1,8 +1,9 @@ -mod config; mod context; +pub mod diesel_schema; +mod errors; mod middleware; +pub mod model; mod routes; mod schema; -pub use config::AUTH_TOKEN; pub use routes::build_router; diff --git a/rust/henka-api/src/main.rs b/rust/henka-api/src/main.rs index 1272183..ab94289 100644 --- a/rust/henka-api/src/main.rs +++ b/rust/henka-api/src/main.rs @@ -3,14 +3,18 @@ use std::net::SocketAddr; use eyre::{Context as EyreContext, eyre}; use henka_api::build_router; use tokio::net::TcpListener; +use tracing_subscriber::fmt::format::FmtSpan; #[tokio::main] async fn main() -> eyre::Result<()> { tracing_subscriber::fmt() - .with_max_level(tracing::Level::INFO) + .with_max_level(tracing::Level::TRACE) + .with_span_events(FmtSpan::NEW | FmtSpan::CLOSE) .init(); - let app = build_router(); + let db_url = std::env::var("DATABASE_URL").expect("not gonna fail"); + + let app = build_router(db_url); let addr = SocketAddr::from(([127, 0, 0, 1], 4000)); let listener = TcpListener::bind(addr) diff --git a/rust/henka-api/src/middleware.rs b/rust/henka-api/src/middleware.rs index 5ef0bcc..571936f 100644 --- a/rust/henka-api/src/middleware.rs +++ b/rust/henka-api/src/middleware.rs @@ -1,22 +1,54 @@ use axum::{body::Body, http::Request, middleware::Next, response::Response}; +use diesel::prelude::*; use tracing::instrument; +use uuid::Uuid; -use crate::config::AUTH_TOKEN; use crate::context::Context; +use crate::model::user::User; -#[instrument] +#[instrument( + skip(request, next), fields(uri = %request.uri(), user))] +#[expect( + clippy::unreachable, + reason = "its really unreachable but maybe there is a better way to solve the issue" +)] pub(crate) async fn auth_middleware( mut request: Request, next: Next, ) -> Result { - let authorized = request + let (pool, current_user) = { + let Some(context) = request.extensions_mut().get_mut::() else { + unreachable!("There should always be a context") + }; + (context.pool.clone(), context.current_user.clone()) + }; + + let maybe_user_id = request .headers() .get("authorization") .and_then(|v| v.to_str().ok()) - .and_then(|s| s.strip_prefix("Bearer ")) - .map(|token| token == AUTH_TOKEN) - .unwrap_or(false); + .and_then(|s| s.strip_prefix("Bearer ")); + + if let Some(user_id) = maybe_user_id { + let user_uuid = Uuid::try_from(user_id).expect("can't fail"); + let conn = pool.get().await.expect("can't fail"); + let users = conn + .interact(move |conn| { + use crate::diesel_schema::users::dsl::*; + + users + .filter(id.eq(user_uuid)) + .select(User::as_select()) + .load(conn) + }) + .await + .expect("can't not fail!") + .expect("can't not fail either!"); + + if users.len() == 1 { + *current_user.lock().expect("shit") = users.first().cloned(); + } + } - request.extensions_mut().insert(Context { authorized }); Ok(next.run(request).await) } diff --git a/rust/henka-api/src/model.rs b/rust/henka-api/src/model.rs new file mode 100644 index 0000000..22d12a3 --- /dev/null +++ b/rust/henka-api/src/model.rs @@ -0,0 +1 @@ +pub mod user; diff --git a/rust/henka-api/src/model/user.rs b/rust/henka-api/src/model/user.rs new file mode 100644 index 0000000..cbd9db3 --- /dev/null +++ b/rust/henka-api/src/model/user.rs @@ -0,0 +1,20 @@ +use diesel::prelude::*; +use uuid::Uuid; + +use crate::diesel_schema::users; + +#[derive(Queryable, Selectable, Debug, Clone)] +#[diesel(table_name = crate::diesel_schema::users)] +#[diesel(check_for_backend(diesel::pg::Pg))] +pub struct User { + pub id: Uuid, + pub nick_name: String, + pub password_hash: String, +} + +#[derive(Insertable, Debug)] +#[diesel(table_name = users)] +pub struct NewUser<'a, 'b> { + pub nick_name: &'a str, + pub password_hash: &'b str, +} diff --git a/rust/henka-api/src/routes.rs b/rust/henka-api/src/routes.rs index f3c86de..9f8fd4b 100644 --- a/rust/henka-api/src/routes.rs +++ b/rust/henka-api/src/routes.rs @@ -1,4 +1,4 @@ -use std::sync::Arc; +use std::sync::{Arc, Mutex}; use axum::{ Extension, Router, @@ -6,14 +6,14 @@ use axum::{ response::Html, routing::{MethodFilter, get, on}, }; -use juniper::EmptyMutation; +use deadpool_diesel::{Manager, Pool, Runtime}; use juniper_axum::{extract::JuniperRequest, graphiql, playground, response::JuniperResponse, ws}; use juniper_graphql_ws::ConnectionConfig; use tracing::instrument; -use crate::context::Context; use crate::middleware::auth_middleware; use crate::schema::{Query, Schema, Subscription}; +use crate::{context::Context, schema::Mutations}; #[instrument(skip_all)] async fn graphql_handler( @@ -33,12 +33,21 @@ async fn homepage() -> Html<&'static str> { .into() } -pub fn build_router() -> Router { - let schema = Arc::new(Schema::new( - Query, - EmptyMutation::::new(), - Subscription, - )); +pub fn build_router(db_url: S) -> Router +where + S: AsRef, +{ + let schema = Arc::new(Schema::new(Query, Mutations {}, Subscription)); + let manager = Manager::new(db_url.as_ref(), Runtime::Tokio1); + let pool = Pool::builder(manager) + .max_size(5) + .build() + .expect("not gonna fail") + .into(); + let context = Context { + current_user: Mutex::new(None).into(), + pool, + }; Router::new() .route( @@ -46,20 +55,13 @@ pub fn build_router() -> Router { on(MethodFilter::GET.or(MethodFilter::POST), graphql_handler), ) .route_layer(from_fn(auth_middleware)) + .layer(Extension(context.clone())) .route( "/subscriptions", - get(ws::>(ConnectionConfig::new(Context { - authorized: false, - }))), - ) - .route( - "/graphiql", - get(graphiql("/graphql", "/subscriptions")), - ) - .route( - "/playground", - get(playground("/graphql", "/subscriptions")), + get(ws::>(ConnectionConfig::new(context.clone()))), ) + .route("/graphiql", get(graphiql("/graphql", "/subscriptions"))) + .route("/playground", get(playground("/graphql", "/subscriptions"))) .route("/", get(homepage)) .layer(Extension(schema)) } diff --git a/rust/henka-api/src/schema.rs b/rust/henka-api/src/schema.rs index 7509d17..83dc3b7 100644 --- a/rust/henka-api/src/schema.rs +++ b/rust/henka-api/src/schema.rs @@ -1,26 +1,66 @@ use std::time::Duration; use futures::stream::{BoxStream, StreamExt}; -use juniper::{EmptyMutation, FieldError, RootNode, graphql_object, graphql_subscription}; +use juniper::{FieldError, FieldResult, RootNode, graphql_object, graphql_subscription}; use tokio::time::interval; use tokio_stream::wrappers::IntervalStream; +use tracing::{Span, instrument}; +use uuid::Uuid; use crate::context::Context; +use crate::errors::{AccessDeniedError, AccountCreationError}; +use crate::schema::me::Me; + +mod me; #[derive(Clone, Copy, Debug)] pub(crate) struct Query; #[graphql_object(context = Context)] impl Query { + #[instrument(fields(result))] fn add(a: i32, b: i32) -> i32 { - a + b + let result = a + b; + + Span::current().record("result", result); + + result } - fn mul(context: &Context, a: i32, b: i32) -> Result { - if !context.authorized { + #[instrument(skip(context), fields(result))] + fn mul(context: &Context, a: i32, b: i32) -> Result, FieldError> { + if context.current_user.lock()?.is_none() { return Err(FieldError::from("unauthorized")); } - Ok(a * b) + + let result = a * b; + + Span::current().record("result", result); + + Ok(Some(result)) + } + + /// The currently logged in user. + /// + /// If no user is logged in `null` will be returned + #[instrument(skip(context), fields(result))] + fn me(context: &Context) -> FieldResult> { + let current_user = context.current_user.lock()?; + + match *current_user { + Some(ref user) => { + Span::current().record("result", user.nick_name.clone()); + + Ok(Some(Me { + id: user.id, + name: user.nick_name.clone(), + })) + } + None => { + Span::current().record("result", ""); + Err(AccessDeniedError::NoUserSession.into()) + } + } } } @@ -41,4 +81,35 @@ impl Subscription { } } -pub(crate) type Schema = RootNode, Subscription>; +pub(crate) struct Mutations {} + +#[graphql_object(context = Context)] +impl Mutations { + #[instrument(skip(password, password_confirm, email, email_confirm), fields(result))] + fn register_user( + context: &Context, + name: String, + email: String, + email_confirm: String, + password: String, + password_confirm: String, + ) -> FieldResult { + if let Some(ref _user) = *context.current_user.lock()? { + Err(AccountCreationError::ForbiddenWhenLoggedIn)?; + }; + + match (email == email_confirm, password == password_confirm) { + (false, false) => Err(AccountCreationError::ConfirmationsDontMatch), + (_, false) => Err(AccountCreationError::PasswordConfirmationDoesntMatch), + (false, _) => Err(AccountCreationError::EmailConfirmationDoesntMatch), + _ => Ok(()), + }?; + + Ok(Me { + id: Uuid::new_v4(), + name, + }) + } +} + +pub(crate) type Schema = RootNode; diff --git a/rust/henka-api/src/schema/me.rs b/rust/henka-api/src/schema/me.rs new file mode 100644 index 0000000..6de9d1b --- /dev/null +++ b/rust/henka-api/src/schema/me.rs @@ -0,0 +1,8 @@ +use juniper::GraphQLObject; +use uuid::Uuid; + +#[derive(Clone, Debug, GraphQLObject)] +pub(crate) struct Me { + pub(crate) id: Uuid, + pub(crate) name: String, +} diff --git a/rust/henka-api/tests/graphql.rs b/rust/henka-api/tests/graphql.rs index e78e9a7..c3ec674 100644 --- a/rust/henka-api/tests/graphql.rs +++ b/rust/henka-api/tests/graphql.rs @@ -1,18 +1,66 @@ +use std::sync::{LazyLock, Once}; + use axum::{Router, body::Body, body::to_bytes, http::StatusCode}; -use henka_api::{AUTH_TOKEN, build_router}; +use diesel::{Connection, PgConnection, pg::Pg, prelude::*}; +use diesel_migrations::{EmbeddedMigrations, MigrationHarness, embed_migrations}; +use henka_api::{build_router, model::user::NewUser}; +use rstest::*; use serde_json::Value; use tower::ServiceExt; +use henka_api::model::user::User; + +static DB_URL: LazyLock = + LazyLock::new(|| std::env::var("TEST_DATABASE_URL").expect("TEST_DATABASE_URL not set")); + +pub const MIGRATIONS: EmbeddedMigrations = embed_migrations!(); + +static INIT: Once = Once::new(); + +pub fn setup_db(conn: &mut C) +where + C: MigrationHarness, +{ + INIT.call_once(|| { + conn.run_pending_migrations(MIGRATIONS) + .expect("should never fail, but its just tests"); + }); +} + +#[fixture] +fn conn() -> PgConnection { + PgConnection::establish(&DB_URL).expect("postgres unreachable") +} + +#[fixture] +fn db(mut conn: PgConnection) { + setup_db(&mut conn); +} + +#[fixture] +fn user(mut conn: PgConnection, _db: ()) -> User { + diesel::insert_into(henka_api::diesel_schema::users::table) + .values(&NewUser { + nick_name: "testuser", + password_hash: "", + }) + .returning(User::as_returning()) + .get_result(&mut conn) + .expect("Error creating testuser") +} + +#[rstest] #[tokio::test] async fn test_add_without_auth() { - let (status, body) = query(build_router(), "{ add(a: 1, b: 2) }", None).await; + let (status, body) = query(build_router(&*DB_URL), "{ add(a: 1, b: 2) }", None).await; assert_eq!(status, StatusCode::OK); assert_eq!(body["data"]["add"], 3); } +#[rstest] #[tokio::test] -async fn test_mul_without_auth_returns_error() { - let (status, body) = query(build_router(), "{ mul(a: 3, b: 4) }", None).await; +async fn test_mul_without_auth_returns_error(_db: ()) { + let (status, body) = query(build_router(&*DB_URL), "{ mul(a: 3, b: 4) }", None).await; assert_eq!(status, StatusCode::OK); assert!( body.get("errors").is_some(), @@ -21,12 +69,19 @@ async fn test_mul_without_auth_returns_error() { assert!(body["data"]["mul"].is_null()); } +#[rstest] #[tokio::test] -async fn test_mul_with_wrong_token_returns_error() { +async fn test_mul_with_wrong_token_returns_error(_db: (), user: User) { + let token = uuid::Uuid::new_v4(); + assert_ne!( + user.id, token, + "Flaky test fuse, UUID is the same by accident!" + ); + let (status, body) = query( - build_router(), + build_router(&*DB_URL), "{ mul(a: 3, b: 4) }", - Some("Bearer wrong-token"), + Some(&token.to_string()), ) .await; assert_eq!(status, StatusCode::OK); @@ -36,9 +91,15 @@ async fn test_mul_with_wrong_token_returns_error() { ); } +#[rstest] #[tokio::test] -async fn test_mul_with_valid_token() { - let (status, body) = query(build_router(), "{ mul(a: 3, b: 4) }", Some(AUTH_TOKEN)).await; +async fn test_mul_with_valid_token(_db: (), user: User) { + let (status, body) = query( + build_router(&*DB_URL), + "{ mul(a: 3, b: 4) }", + Some(&user.id.to_string()), + ) + .await; assert_eq!(status, StatusCode::OK); assert_eq!(body["data"]["mul"], 12); } -- 2.51.2