diff --git a/.gitea/workflows/deploy_api.yml b/.gitea/workflows/deploy_api.yml new file mode 100644 index 0000000..99d0187 --- /dev/null +++ b/.gitea/workflows/deploy_api.yml @@ -0,0 +1,49 @@ +name: Deploy API + +on: + push: + branches: + - master + - multi-hotel-refactor + +jobs: + build-and-deploy: + runs-on: ubuntu-latest + + steps: + - name : Checkout + uses: actions/checkout@v4 + + - name: Build Docker image + run: | + docker build -t hotel-demo:latest . + + - name: Save Docker image + run: | + docker save hotel-demo:latest -o hotel-demo.tar + + + - name: setup SSH + run: | + mkdir -p ~/.ssh + echo "${{ secrets.DEPLOY_KEY }}" > ~/.ssh/id_ed25519 + chmod 600 ~/.ssh/id_ed25519 + ssh-keyscan 79.137.75.155 >> ~/.ssh/known_hosts + + #Upload + scp hotel-demo.tar deploy@79.137.75.155:/tmp + + docker load -i hotel-demo.tar + + docker stop hotel-demo || true + docker rm hotel-demo || true + + docker run -d \ + --name hotel-demo \ + -e JWT_SECRET=${{ secrets.JWT_SECRET }} \ + -v "/var/lib/hotel-demo/:/app/db" \ + -p 5090:7080 \ + hotel-demo:latest + + rm hotel-demo:latest + \ No newline at end of file diff --git a/.gitignore b/.gitignore index ec47942..c3fcca0 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,5 @@ /target +/.vscode + .env \ No newline at end of file diff --git a/.vscode/settings.json b/.vscode/settings.json deleted file mode 100644 index a0a3934..0000000 --- a/.vscode/settings.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "todo-tree.tree.showBadges": true -} \ No newline at end of file diff --git a/.vscode/tasks.json b/.vscode/tasks.json deleted file mode 100644 index 88381fe..0000000 --- a/.vscode/tasks.json +++ /dev/null @@ -1,14 +0,0 @@ -{ - "version": "2.0.0", - "tasks": [ - { - "type": "cargo", - "command": "build", - "problemMatcher": [ - "$rustc" - ], - "group": "build", - "label": "rust: cargo build" - } - ] -} \ No newline at end of file diff --git a/Cargo.lock b/Cargo.lock index 8cbfea3..fa3d032 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -255,6 +255,12 @@ version = "1.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2fd1289c04a9ea8cb22300a459a72a385d7c73d3259e2ed7dcb2af674838cfa9" +[[package]] +name = "cfg_aliases" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724" + [[package]] name = "chrono" version = "0.4.42" @@ -265,7 +271,7 @@ dependencies = [ "js-sys", "num-traits", "wasm-bindgen", - "windows-link 0.2.0", + "windows-link", ] [[package]] @@ -289,16 +295,6 @@ dependencies = [ "version_check", ] -[[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" @@ -387,31 +383,6 @@ version = "0.15.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1aaf95b3e5c8f23aa320147307562d361db0ae0d51242340f558153b4eb2439b" -[[package]] -name = "encoding_rs" -version = "0.8.35" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "75030f3c4f45dafd7586dd6780965a8c7e8e285a5ecb86713e63a79c5b2766f3" -dependencies = [ - "cfg-if", -] - -[[package]] -name = "equivalent" -version = "1.0.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" - -[[package]] -name = "errno" -version = "0.3.14" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" -dependencies = [ - "libc", - "windows-sys 0.59.0", -] - [[package]] name = "fallible-iterator" version = "0.3.0" @@ -424,12 +395,6 @@ version = "0.1.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7360491ce676a36bf9bb3c56c1aa791658183a54d2744120f27285738d90465a" -[[package]] -name = "fastrand" -version = "2.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be" - [[package]] name = "find-msvc-tools" version = "0.1.2" @@ -448,21 +413,6 @@ version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2" -[[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.2" @@ -564,9 +514,11 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "26145e563e54f2cadc477553f1ec5ee650b00862f0a58bcd12cbdc5f0ea2d2f4" dependencies = [ "cfg-if", + "js-sys", "libc", "r-efi", "wasi 0.14.7+wasi-0.2.4", + "wasm-bindgen", ] [[package]] @@ -575,25 +527,6 @@ version = "0.31.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "07e28edb80900c19c28f1072f2e8aeca7fa06b23cd4169cefe1af5aa3260783f" -[[package]] -name = "h2" -version = "0.4.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f3c0b69cfcb4e1b9f1bf2f53f95f766e4661169728ec61cd3fe5a0166f2d1386" -dependencies = [ - "atomic-waker", - "bytes", - "fnv", - "futures-core", - "futures-sink", - "http", - "indexmap", - "slab", - "tokio", - "tokio-util", - "tracing", -] - [[package]] name = "hashbrown" version = "0.14.5" @@ -609,12 +542,6 @@ dependencies = [ "foldhash", ] -[[package]] -name = "hashbrown" -version = "0.16.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5419bdc4f6a9207fbeba6d11b604d481addf78ecd10c11ad51e76c2f6482748d" - [[package]] name = "hashlink" version = "0.10.0" @@ -672,6 +599,7 @@ dependencies = [ "serde", "serde_json", "tokio", + "tower-http", "uuid", ] @@ -731,7 +659,6 @@ dependencies = [ "bytes", "futures-channel", "futures-core", - "h2", "http", "http-body", "httparse", @@ -758,22 +685,7 @@ dependencies = [ "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", + "webpki-roots", ] [[package]] @@ -795,11 +707,9 @@ dependencies = [ "percent-encoding", "pin-project-lite", "socket2", - "system-configuration", "tokio", "tower-service", "tracing", - "windows-registry", ] [[package]] @@ -928,16 +838,6 @@ dependencies = [ "icu_properties", ] -[[package]] -name = "indexmap" -version = "2.12.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6717a8d2a5a929a1a2eb43a12812498ed141a0bcfb7e8f7844fbdbe4303bba9f" -dependencies = [ - "equivalent", - "hashbrown 0.16.0", -] - [[package]] name = "inout" version = "0.1.4" @@ -1022,12 +922,6 @@ dependencies = [ "vcpkg", ] -[[package]] -name = "linux-raw-sys" -version = "0.11.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "df1d3c3b53da64cf5760482273a98e575c651a67eec7f77df96b5b642de8f039" - [[package]] name = "litemap" version = "0.8.1" @@ -1050,6 +944,12 @@ version = "0.4.28" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "34080505efa8e45a4b816c349525ebe327ceaa8559756f0356cba97ef3bf7432" +[[package]] +name = "lru-slab" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "112b39cec0b298b6c1999fee3e31427f74f676e4cb9879ed1a121b43661a4154" + [[package]] name = "matchit" version = "0.8.4" @@ -1088,23 +988,6 @@ dependencies = [ "windows-sys 0.59.0", ] -[[package]] -name = "native-tls" -version = "0.2.14" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "87de3442987e9dbec73158d5c715e7ad9072fda936bb03d19d7fa10e00520f0e" -dependencies = [ - "libc", - "log", - "openssl", - "openssl-probe", - "openssl-sys", - "schannel", - "security-framework", - "security-framework-sys", - "tempfile", -] - [[package]] name = "num-bigint" version = "0.4.6" @@ -1154,50 +1037,6 @@ version = "1.21.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" -[[package]] -name = "openssl" -version = "0.10.75" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "08838db121398ad17ab8531ce9de97b244589089e290a384c900cb9ff7434328" -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.111" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "82cab2d520aa75e3c58898289429321eb788c3106963d0dc886ec7a5f4adc321" -dependencies = [ - "cc", - "libc", - "pkg-config", - "vcpkg", -] - [[package]] name = "parking_lot" version = "0.12.4" @@ -1299,6 +1138,61 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "quinn" +version = "0.11.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9e20a958963c291dc322d98411f541009df2ced7b5a4f2bd52337638cfccf20" +dependencies = [ + "bytes", + "cfg_aliases", + "pin-project-lite", + "quinn-proto", + "quinn-udp", + "rustc-hash", + "rustls", + "socket2", + "thiserror", + "tokio", + "tracing", + "web-time", +] + +[[package]] +name = "quinn-proto" +version = "0.11.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1906b49b0c3bc04b5fe5d86a77925ae6524a19b816ae38ce1e426255f1d8a31" +dependencies = [ + "bytes", + "getrandom 0.3.3", + "lru-slab", + "rand", + "ring", + "rustc-hash", + "rustls", + "rustls-pki-types", + "slab", + "thiserror", + "tinyvec", + "tracing", + "web-time", +] + +[[package]] +name = "quinn-udp" +version = "0.5.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "addec6a0dcad8a8d96a771f815f0eaf55f9d1805756410b39f5fa81332574cbd" +dependencies = [ + "cfg_aliases", + "libc", + "once_cell", + "socket2", + "tracing", + "windows-sys 0.59.0", +] + [[package]] name = "quote" version = "1.0.40" @@ -1385,37 +1279,34 @@ dependencies = [ [[package]] name = "reqwest" -version = "0.12.24" +version = "0.12.28" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9d0946410b9f7b082a427e4ef5c8ff541a88b357bc6c637c40db3a68ac70a36f" +checksum = "eddd3ca559203180a307f12d114c268abf583f59b03cb906fd0b3ff8646c1147" dependencies = [ "base64", "bytes", - "encoding_rs", "futures-channel", "futures-core", "futures-util", - "h2", "http", "http-body", "http-body-util", "hyper", "hyper-rustls", - "hyper-tls", "hyper-util", "js-sys", "log", - "mime", - "native-tls", "percent-encoding", "pin-project-lite", + "quinn", + "rustls", "rustls-pki-types", "serde", "serde_json", "serde_urlencoded", "sync_wrapper", "tokio", - "tokio-native-tls", + "tokio-rustls", "tower", "tower-http", "tower-service", @@ -1423,6 +1314,7 @@ dependencies = [ "wasm-bindgen", "wasm-bindgen-futures", "web-sys", + "webpki-roots", ] [[package]] @@ -1460,17 +1352,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "56f7d92ca342cea22a06f2121d944b4fd82af56988c270852495420f961d4ace" [[package]] -name = "rustix" -version = "1.1.2" +name = "rustc-hash" +version = "2.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cd15f8a2c5551a84d56efdc1cd049089e409ac19a3072d5037a17fd70719ff3e" -dependencies = [ - "bitflags", - "errno", - "libc", - "linux-raw-sys", - "windows-sys 0.59.0", -] +checksum = "357703d41365b4b27c590e3ed91eabb1b663f07c4c084095e60cbed4362dff0d" [[package]] name = "rustls" @@ -1479,6 +1364,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "533f54bc6a7d4f647e46ad909549eda97bf5afc1585190ef692b4286b198bd8f" dependencies = [ "once_cell", + "ring", "rustls-pki-types", "rustls-webpki", "subtle", @@ -1491,6 +1377,7 @@ version = "1.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "94182ad936a0c91c324cd46c6511b9510ed16af436d7b5bab34beab0afd55f7a" dependencies = [ + "web-time", "zeroize", ] @@ -1517,15 +1404,6 @@ version = "1.0.20" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "28d3b2b1366ec20994f1fd18c3c594f05c5dd4bc44d8bb0c1c632c8d6829481f" -[[package]] -name = "schannel" -version = "0.1.28" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "891d81b926048e76efe18581bf793546b4c0eaf8448d72be8de2bbee5fd166e1" -dependencies = [ - "windows-sys 0.61.1", -] - [[package]] name = "scheduled-thread-pool" version = "0.2.7" @@ -1541,29 +1419,6 @@ version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" -[[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.15.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cc1f0cbffaac4852523ce30d8bd3c5cdc873501d96ff467ca09b6767bb8cd5c0" -dependencies = [ - "core-foundation-sys", - "libc", -] - [[package]] name = "serde" version = "1.0.226" @@ -1733,40 +1588,6 @@ 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 = "tempfile" -version = "3.23.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2d31c77bdf42a745371d260a26ca7163f1e0924b64afa0b688e61b5a9fa02f16" -dependencies = [ - "fastrand", - "getrandom 0.3.3", - "once_cell", - "rustix", - "windows-sys 0.59.0", -] - [[package]] name = "thiserror" version = "2.0.16" @@ -1828,6 +1649,21 @@ dependencies = [ "zerovec", ] +[[package]] +name = "tinyvec" +version = "1.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bfa5fdc3bce6191a1dbc8c02d5c8bffcf557bafa17c124c5264a458f1b0613fa" +dependencies = [ + "tinyvec_macros", +] + +[[package]] +name = "tinyvec_macros" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" + [[package]] name = "tokio" version = "1.47.1" @@ -1859,16 +1695,6 @@ 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.4" @@ -1891,19 +1717,6 @@ dependencies = [ "tungstenite", ] -[[package]] -name = "tokio-util" -version = "0.7.17" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2efa149fe76073d6e8fd97ef4f4eca7b67f599660115591483572e406e165594" -dependencies = [ - "bytes", - "futures-core", - "futures-sink", - "pin-project-lite", - "tokio", -] - [[package]] name = "tower" version = "0.5.2" @@ -1922,9 +1735,9 @@ dependencies = [ [[package]] name = "tower-http" -version = "0.6.6" +version = "0.6.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "adc82fd73de2a9722ac5da747f12383d2bfdb93591ee6c58486e0097890f05f2" +checksum = "d4e6559d53cc268e5031cd8429d05415bc4cb4aefc4aa5d6cc35fbf5b924a1f8" dependencies = [ "bitflags", "bytes", @@ -2175,6 +1988,25 @@ dependencies = [ "wasm-bindgen", ] +[[package]] +name = "web-time" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a6580f308b1fad9207618087a65c04e7a10bc77e02c8e84e9b00dd4b12fa0bb" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "webpki-roots" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b2878ef029c47c6e8cf779119f20fcf52bde7ad42a731b2a304bc221df17571e" +dependencies = [ + "rustls-pki-types", +] + [[package]] name = "windows-core" version = "0.62.0" @@ -2183,9 +2015,9 @@ checksum = "57fe7168f7de578d2d8a05b07fd61870d2e73b4020e9f49aa00da8471723497c" dependencies = [ "windows-implement", "windows-interface", - "windows-link 0.2.0", - "windows-result 0.4.0", - "windows-strings 0.5.0", + "windows-link", + "windows-result", + "windows-strings", ] [[package]] @@ -2210,54 +2042,19 @@ dependencies = [ "syn", ] -[[package]] -name = "windows-link" -version = "0.1.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5e6ad25900d524eaabdbbb96d20b4311e1e7ae1699af4fb28c17ae66c80d798a" - [[package]] name = "windows-link" version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "45e46c0661abb7180e7b9c281db115305d49ca1709ab8242adf09666d2173c65" -[[package]] -name = "windows-registry" -version = "0.5.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5b8a9ed28765efc97bbc954883f4e6796c33a06546ebafacbabee9696967499e" -dependencies = [ - "windows-link 0.1.3", - "windows-result 0.3.4", - "windows-strings 0.4.2", -] - -[[package]] -name = "windows-result" -version = "0.3.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "56f42bd332cc6c8eac5af113fc0c1fd6a8fd2aa08a0119358686e5160d0586c6" -dependencies = [ - "windows-link 0.1.3", -] - [[package]] name = "windows-result" version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7084dcc306f89883455a206237404d3eaf961e5bd7e0f312f7c91f57eb44167f" dependencies = [ - "windows-link 0.2.0", -] - -[[package]] -name = "windows-strings" -version = "0.4.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "56e6c93f3a0c3b36176cb1327a4958a0353d5d166c2a35cb268ace15e91d3b57" -dependencies = [ - "windows-link 0.1.3", + "windows-link", ] [[package]] @@ -2266,7 +2063,7 @@ version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7218c655a553b0bed4426cf54b20d7ba363ef543b52d515b3e48d7fd55318dda" dependencies = [ - "windows-link 0.2.0", + "windows-link", ] [[package]] @@ -2287,15 +2084,6 @@ dependencies = [ "windows-targets", ] -[[package]] -name = "windows-sys" -version = "0.61.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6f109e41dd4a3c848907eb83d5a42ea98b3769495597450cf6d153507b166f0f" -dependencies = [ - "windows-link 0.2.0", -] - [[package]] name = "windows-targets" version = "0.52.6" diff --git a/Cargo.toml b/Cargo.toml index 7654ff2..bba5d95 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -29,7 +29,8 @@ rand_core = {version = "0.6.4", features = ["getrandom"]} futures-util = {version = "0.3.31"} uuid = {version = "1.18.1", features = ["serde"] } base64 = "0.22.1" -reqwest = { version = "0.12.24", features = ["json","blocking"] } +reqwest = { version = "0.12.28", default-features = false, features = ["json","blocking", "rustls-tls"] } +tower-http = { version = "0.6.7", features = ["cors"] } diff --git a/Dockerfile copy b/Dockerfile copy deleted file mode 100644 index b8f64fe..0000000 --- a/Dockerfile copy +++ /dev/null @@ -1,16 +0,0 @@ -FROM rust:latest AS builder -WORKDIR /app -COPY . . -RUN cargo build --release - -FROM debian:bookworm-slim -WORKDIR /app -COPY --from=builder /app/target/release/hotel-api-rs /usr/local/bin/hotel-api-rs - -# Create the directory where DB will be stored -RUN mkdir -p /db - -# Expose API port -EXPOSE 8080 - -CMD ["/usr/local/bin/hotel-api-rs"] \ No newline at end of file diff --git a/authbruh.rs b/authbruh.rs deleted file mode 100644 index 3701d3f..0000000 --- a/authbruh.rs +++ /dev/null @@ -1,37 +0,0 @@ -use std::time::Duration; -use axum::{ - body::{to_bytes, Body}, - http::{Request as HttpRequest, StatusCode}, - middleware::Next, - response::{Response, IntoResponse}, - Json, - extract::{Path, State, FromRequest} -}; -use axum::extract::Request as ExtractRequest; -use jsonwebtoken::{decode, DecodingKey, Validation, encode, EncodingKey, Header}; -use serde::{Deserialize, Serialize}; -use serde_json::Value; -use chrono::{Utc}; -use rusqlite::{params, Connection, OptionalExtension}; - -//use crate::utils::db_pool::; -use crate::utils::db_pool::{ - HotelPool, - AppState, -}; - - -pub async fn clean_auth_loging( - State(state): State, - // LoginPayload(payload): LoginPayload, -) -> impl IntoResponse { - // You can access state.pool and state.jwt_secret here - format!("Got secret: {}", state.jwt_secret) -} - - -fn internal_error(err: E) -> (StatusCode, String) { - (StatusCode::INTERNAL_SERVER_ERROR, format!("Internal error: {}", err)) -} - - diff --git a/db/1.sqlite b/db/1.sqlite index 369beb4..6db1f38 100644 Binary files a/db/1.sqlite and b/db/1.sqlite differ diff --git a/db/1.sqlite-shm b/db/1.sqlite-shm deleted file mode 100644 index fe9ac28..0000000 Binary files a/db/1.sqlite-shm and /dev/null differ diff --git a/db/1.sqlite-wal b/db/1.sqlite-wal deleted file mode 100644 index e69de29..0000000 diff --git a/db/2.sqlite b/db/2.sqlite deleted file mode 100644 index e69de29..0000000 diff --git a/db/auth.sqlite-shm b/db/auth.sqlite-shm deleted file mode 100644 index c1b1e8d..0000000 Binary files a/db/auth.sqlite-shm and /dev/null differ diff --git a/db/auth.sqlite-wal b/db/auth.sqlite-wal deleted file mode 100644 index 52711c3..0000000 Binary files a/db/auth.sqlite-wal and /dev/null differ diff --git a/db/auth copy 2.sqlite b/db/auth_copy_2.sqlite similarity index 68% rename from db/auth copy 2.sqlite rename to db/auth_copy_2.sqlite index 978d5ed..a591b40 100644 Binary files a/db/auth copy 2.sqlite and b/db/auth_copy_2.sqlite differ diff --git a/db/auth_demo.sqlite b/db/auth_demo.sqlite new file mode 100644 index 0000000..fd5c2e9 Binary files /dev/null and b/db/auth_demo.sqlite differ diff --git a/db/demo-db/1 copy.sqlite b/db/demo-db/1 copy.sqlite new file mode 100644 index 0000000..d3d949b Binary files /dev/null and b/db/demo-db/1 copy.sqlite differ diff --git a/db/demo-db/auth_demo.sqlite b/db/demo-db/auth_demo.sqlite new file mode 100644 index 0000000..fd5c2e9 Binary files /dev/null and b/db/demo-db/auth_demo.sqlite differ diff --git a/db/hotel_schema.sql b/db/hotel_schema.sql deleted file mode 100644 index be20ae9..0000000 --- a/db/hotel_schema.sql +++ /dev/null @@ -1,51 +0,0 @@ -CREATE TABLE rooms ( - id INTEGER PRIMARY KEY AUTOINCREMENT, - number TEXT NOT NULL UNIQUE, - status TEXT NOT NULL DEFAULT 'clean' -); - -CREATE TABLE IF NOT EXISTS "conversation" ( - `id` INTEGER PRIMARY KEY AUTOINCREMENT UNIQUE NOT NULL, - `creator_id` INTEGER, - `title` TEXT, - `created_at` TEXT DEFAULT (CURRENT_TIMESTAMP) -); - -CREATE TABLE IF NOT EXISTS "message" ( - `id` INTEGER PRIMARY KEY AUTOINCREMENT, - `conversation_id` INTEGER REFERENCES `conversation`(`id`), - `sender_id` INTEGER NOT NULL, `content` TEXT NOT NULL, - `sent_at` TEXT DEFAULT (CURRENT_TIMESTAMP) -); - -CREATE TABLE "conversation_participants" ( - `conversation_id` INTEGER NOT NULL REFERENCES `conversation`(`id`), - `user_id` INTEGER NOT NULL, - `joined_at` TEXT DEFAULT (CURRENT_TIMESTAMP) - PRIMARY KEY (conversation_id, user_id) -); - -CREATE TABLE IF NOT EXISTS "inventory" ( - `id` INTEGER PRIMARY KEY AUTOINCREMENT, - `amount` INTEGER, - `item_name` TEXT, - `user_id` INT, - `updated_at` TEXT DEFAULT (CURRENT_TIMESTAMP) - ); - -CREATE TABLE IF NOT EXISTS "room_history" ( - `id` INTEGER PRIMARY KEY AUTOINCREMENT, - `room_id` INTEGER NOT NULL REFERENCES `rooms`(`id`), - `room_number` TEXT NOT NULL, - `status` TEXT, - `updated_at` TEXT DEFAULT CURRENT_TIMESTAMP -); - -CREATE TABLE IF NOT EXISTS "inventory_history" ( - `id` INTEGER PRIMARY KEY AUTOINCREMENT UNIQUE NOT NULL, - `item_id` INTEGER NOT NULL REFERENCES `inventory`(`id`), - `amount` INTEGER NOT NULL, - `item_name` TEXT, - `user_id` INT, - `updated_at` TEXT DEFAULT (CURRENT_TIMESTAMP)); - diff --git a/db/test_backup/2 copy.sqlite b/db/test_backup/2 copy.sqlite new file mode 100644 index 0000000..d3d949b Binary files /dev/null and b/db/test_backup/2 copy.sqlite differ diff --git a/db/auth.sqlite b/db/test_backup/auth_2.sqlite similarity index 91% rename from db/auth.sqlite rename to db/test_backup/auth_2.sqlite index 4e846d8..0662d3b 100644 Binary files a/db/auth.sqlite and b/db/test_backup/auth_2.sqlite differ diff --git a/hotel_1.sqlite b/hotel_1.sqlite deleted file mode 100644 index e69de29..0000000 diff --git a/hotel_schema.sql b/hotel_schema.sql deleted file mode 100644 index e69de29..0000000 diff --git a/refrech_token.sql b/refrech_token.sql deleted file mode 100644 index 6adcce2..0000000 --- a/refrech_token.sql +++ /dev/null @@ -1,6 +0,0 @@ -id INT - user_id INTEGER NOT NULL, - token_hash TEXT NOT NULL, - device_id TEXT NOT NULL, - user_agent TEXT NOT NULL, - hotel_id TEXT \ No newline at end of file diff --git a/rust-api-1-0-2.tar b/rust-api-1-0-2.tar deleted file mode 100644 index b376aac..0000000 Binary files a/rust-api-1-0-2.tar and /dev/null differ diff --git a/src/chat/extractor.rs b/src/chat/extractor.rs index 4041c4a..ffb5f84 100644 --- a/src/chat/extractor.rs +++ b/src/chat/extractor.rs @@ -1,25 +1,21 @@ use axum::{ - extract::{ - FromRequest, Request - }, - http::StatusCode, Json, + extract::{FromRequest, Request}, + http::StatusCode, }; -use chrono::NaiveDateTime; use serde::Deserialize; - - #[derive(Deserialize, Debug)] -pub struct CreateConversationValues{ +pub struct CreateConversationValues { //pub creator_id: i32, // already in token ? - pub title: String, + pub name: String, } pub struct CreateConversationPayload(pub CreateConversationValues); impl FromRequest for CreateConversationPayload -where S: Send + Sync, +where + S: Send + Sync, { type Rejection = (StatusCode, String); @@ -27,23 +23,22 @@ where S: Send + Sync, let Json(payload) = Json::::from_request(req, state) .await .map_err(|err| (StatusCode::BAD_REQUEST, format!("Invalid body: {}", err)))?; - + Ok(CreateConversationPayload(payload)) - } } - #[derive(Deserialize, Debug)] -pub struct AddUserConversationValues{ +pub struct AddUserConversationValues { pub conv_id: u32, - pub users: Vec + pub users: Vec, } pub struct AddUserConversationPayload(pub AddUserConversationValues); impl FromRequest for AddUserConversationPayload -where S: Send + Sync, +where + S: Send + Sync, { type Rejection = (StatusCode, String); @@ -51,14 +46,13 @@ where S: Send + Sync, let Json(payload) = Json::::from_request(req, state) .await .map_err(|err| (StatusCode::BAD_REQUEST, format!("Invalid body: {}", err)))?; - + Ok(AddUserConversationPayload(payload)) - } } #[derive(Deserialize, Debug)] -pub struct SendMessageValues{ +pub struct SendMessageValues { pub conv_id: u32, pub message: String, } @@ -66,7 +60,8 @@ pub struct SendMessageValues{ pub struct SendMessagePayload(pub SendMessageValues); impl FromRequest for SendMessagePayload -where S: Send + Sync, +where + S: Send + Sync, { type Rejection = (StatusCode, String); @@ -74,23 +69,22 @@ where S: Send + Sync, let Json(payload) = Json::::from_request(req, state) .await .map_err(|err| (StatusCode::BAD_REQUEST, format!("Invalid body: {}", err)))?; - + Ok(SendMessagePayload(payload)) - } } - #[derive(Deserialize, Debug)] -pub struct GetMessagesValues{ +pub struct GetMessagesValues { pub conv_id: u32, - pub timestamp :Option, + pub timestamp: Option, } pub struct GetMessagesPayload(pub GetMessagesValues); impl FromRequest for GetMessagesPayload -where S: Send + Sync, +where + S: Send + Sync, { type Rejection = (StatusCode, String); @@ -98,9 +92,7 @@ where S: Send + Sync, let Json(payload) = Json::::from_request(req, state) .await .map_err(|err| (StatusCode::BAD_REQUEST, format!("Invalid body: {}", err)))?; - + Ok(GetMessagesPayload(payload)) - } } - diff --git a/src/chat/handlers.rs b/src/chat/handlers.rs index 73d182a..8cb3ef8 100644 --- a/src/chat/handlers.rs +++ b/src/chat/handlers.rs @@ -1,183 +1,376 @@ - -use std::collections::HashMap; +use std::collections::{HashMap, HashSet}; use axum::{ - Json, extract::{ - FromRequest,FromRequestParts, State, - - }, http::StatusCode, response::{IntoResponse, sse::KeepAlive} - + Json, + extract::{FromRequest, FromRequestParts, Path, State}, + http::StatusCode, + response::{IntoResponse, sse::KeepAlive}, }; //use axum::extract::ws::Message; -use chrono::NaiveDateTime; -use rusqlite::{Name, params}; +use rusqlite::OptionalExtension; +use rusqlite::{Name, Statement, params}; use serde::Serialize; use serde_json::{json, to_value}; use crate::chat::extractor::{ -AddUserConversationPayload, CreateConversationPayload, GetMessagesPayload, SendMessagePayload + AddUserConversationPayload, CreateConversationPayload, GetMessagesPayload, SendMessagePayload, }; -use crate::utils::db_pool::{AppState}; use crate::utils::auth::AuthClaims; +use crate::utils::db_pool::AppState; - - +//TODO: update conversation title +//FIXME: make a default title if empty pub async fn create_conversation( State(state): State, - AuthClaims {user_id, hotel_id}: AuthClaims, - CreateConversationPayload(payload): CreateConversationPayload + AuthClaims { user_id, hotel_id }: AuthClaims, + CreateConversationPayload(payload): CreateConversationPayload, ) -> impl IntoResponse { - let pool = state.hotel_pools.get_pool(hotel_id); - let conn = match pool.get(){ + let conn = match pool.get() { Ok(conn) => conn, - Err(err) => return (StatusCode::INTERNAL_SERVER_ERROR, format!("Pool error")) + Err(err) => return (StatusCode::INTERNAL_SERVER_ERROR, format!("Pool error")), }; let result = conn.execute( - "INSERT INTO conversation (creator_id, title) VALUES (?1, ?2)", - params![&user_id, &payload.title], -); + "INSERT INTO conversation (creator_id, name) VALUES (?1, ?2)", + params![&user_id, &payload.name], + ); - match result { - Ok(rows) if rows > 0 => (StatusCode::OK, format!("Created conversation {}", payload.title)), - Ok(_) => (StatusCode::NOT_FOUND, "not able to create the conversation".to_string() ), - Err(err) => (StatusCode::INTERNAL_SERVER_ERROR, format!("Error when creating the conversation : {err}")), - + let rows = match result { + Ok(rows) => rows, + Err(err) => { + return ( + StatusCode::INTERNAL_SERVER_ERROR, + format!("Error when creating the conversation: {err}"), + ); + } + }; + + if rows == 0 { + return ( + StatusCode::NOT_FOUND, + "not able to create the conversation".to_string(), + ); } + + let conv_id = conn.last_insert_rowid(); + + let user_conv_rel = conn.execute( + "INSERT INTO conversation_participants (conversation_id, user_id, name) + VALUES (?1, ?2, ?3)", + params![conv_id, user_id, payload.name], + ); + + match user_conv_rel { + Ok(r) => { + return ( + StatusCode::OK, + format!("Created conversation {}", payload.name), + ); + } + Err(err) => { + return ( + StatusCode::INTERNAL_SERVER_ERROR, + format!("Error when creating the conversation: {err}"), + ); + } + } + + // extra logic here (more queries, validations, etc.) + + //(StatusCode::OK, format!("Created conversation {}", payload.name)) } +//FIXME: add title to conv pub async fn add_user_to_conv( State(state): State, - AuthClaims {user_id, hotel_id}: AuthClaims, - AddUserConversationPayload(payload):AddUserConversationPayload + AuthClaims { user_id, hotel_id }: AuthClaims, + AddUserConversationPayload(payload): AddUserConversationPayload, ) -> impl IntoResponse { - let pool = state.hotel_pools.get_pool(hotel_id); - let conn = match pool.get(){ + let mut conn = match pool.get() { Ok(conn) => conn, - Err(err) => return (StatusCode::INTERNAL_SERVER_ERROR, format!("Pool error")) + Err(err) => return (StatusCode::INTERNAL_SERVER_ERROR, format!("Pool error")), }; - let mut statement = match conn.prepare( - "SELECT 1 FROM conversation WHERE creator_id = ?1 AND id = ?2" , - ){ - Ok(statement) => statement, - Err(_) => return (StatusCode::INTERNAL_SERVER_ERROR, "prepare failed".to_string()) - }; - - match statement.exists(params![user_id, payload.conv_id]) { - Ok(true) => { - //user is creator - } - Ok(false) => { - //user is not the creator - return (StatusCode::FORBIDDEN, "Not the creato of the conversation".to_string()) - } + let creator_name: Option = match conn + .query_row( + "SELECT name FROM conversation WHERE creator_id = ?1 AND id = ?2", + params![user_id, payload.conv_id], + |row| row.get(0), + ) + .optional() + { + Ok(name) => name, + //Ok(None) => false, Err(_) => { - return(StatusCode::INTERNAL_SERVER_ERROR, "Query failed".to_string()) + return ( + StatusCode::INTERNAL_SERVER_ERROR, + "Creator check failed".to_string(), + ); } + }; + let is_creator = creator_name.is_some(); + + if !is_creator { + return ( + StatusCode::FORBIDDEN, + "Not the creator of the conversation".to_string(), + ); } - for target_id in &payload.users { - let rows_inserted = match conn.execute( - "INSERT INTO conversation_participants (conversation_id, user_id) VALUES (?1, ?2)", - params![payload.conv_id, target_id], - ) { - Ok(n) => n, - Err(err) => { - return (StatusCode::INTERNAL_SERVER_ERROR, format!("Err adding user {}: {}", target_id, err)); + //fix this + let existing: HashSet = { + let mut stmt = match conn + .prepare("SELECT user_id FROM conversation_participants WHERE conversation_id = ?1") + { + Ok(s) => s, + Err(_) => { + return ( + StatusCode::INTERNAL_SERVER_ERROR, + "Prepare participants stmt failed".to_string(), + ); } }; - if rows_inserted == 0 { - return (StatusCode::NOT_FOUND, format!("Could not add user {}", target_id)); + match stmt.query_map(params![payload.conv_id], |row| row.get(0)) { + Ok(rows) => rows.filter_map(Result::ok).collect(), + Err(_) => { + return ( + StatusCode::INTERNAL_SERVER_ERROR, + "Query participants failed".to_string(), + ); + } + } + }; // ← stmt dropped HERE + + let payload_users: HashSet = payload.users.into_iter().collect(); + + let to_add: Vec = payload_users.difference(&existing).copied().collect(); + + let to_remove: Vec = existing.difference(&payload_users).copied().collect(); + + let tx = match conn.transaction() { + Ok(t) => t, + Err(_) => { + return ( + StatusCode::INTERNAL_SERVER_ERROR, + "Transaction start failed for update conv participants".to_string(), + ); + } + }; + + for user_id in to_add { + if let Err(err) = tx.execute( + "INSERT INTO conversation_participants (conversation_id, user_id, name) + VALUES (?1, ?2, ?3)", + params![payload.conv_id, user_id, creator_name], + ) { + return ( + StatusCode::INTERNAL_SERVER_ERROR, + format!("Insert failed for {}: {}", user_id, err), + ); } } - return (StatusCode::OK, "ok".to_string()); + for user_id in to_remove { + if let Err(err) = tx.execute( + "DELETE FROM conversation_participants + WHERE conversation_id = ?1 AND user_id = ?2", + params![payload.conv_id, user_id], + ) { + return ( + StatusCode::INTERNAL_SERVER_ERROR, + format!("Delete failed for {}: {}", user_id, err), + ); + } + } + if let Err(_) = tx.commit() { + return ( + StatusCode::INTERNAL_SERVER_ERROR, + "Transaction commit failed".to_string(), + ); + } + + return (StatusCode::OK, "ok".to_string()); +} + +/* +#[derive(Deserialize, Serialize, Debug)] +pub struct Get + +*/ + +pub async fn get_conv_users( + State(state): State, + AuthClaims { user_id, hotel_id }: AuthClaims, + Path(conv_id): Path, +) -> impl IntoResponse { + let pool = state.hotel_pools.get_pool(hotel_id); + + let conn = match pool.get() { + Ok(c) => c, + Err(err) => { + return ( + StatusCode::INTERNAL_SERVER_ERROR, + format!("Error opening pol connection : {err}"), + ); + } + }; + + let mut stmt = match conn + .prepare("SELECT conversation_id, name FROM conversation_participants WHERE user_id = ?1") + { + Ok(s) => s, + Err(e) => { + return ( + StatusCode::INTERNAL_SERVER_ERROR, + format!("Prepare failed: {}", e), + ); + } + }; + + //fix this + let existing: HashSet = { + let mut stmt = match conn + .prepare("SELECT user_id FROM conversation_participants WHERE conversation_id = ?1") + { + Ok(s) => s, + Err(_) => { + return ( + StatusCode::INTERNAL_SERVER_ERROR, + "Prepare participants stmt failed".to_string(), + ); + } + }; + + match stmt.query_map(params![conv_id], |row| row.get(0)) { + Ok(rows) => rows.filter_map(Result::ok).collect(), + Err(_) => { + return ( + StatusCode::INTERNAL_SERVER_ERROR, + "Query participants failed".to_string(), + ); + } + } + }; // ← stmt dropped HERE + + let present: Vec = existing.into_iter().collect(); + + match serde_json::to_string(&present) { + Ok(json) => (StatusCode::OK, json), + Err(e) => ( + StatusCode::INTERNAL_SERVER_ERROR, + format!("Serialization failed: {}", e), + ), + } } pub async fn send_message( State(state): State, - AuthClaims {user_id, hotel_id}: AuthClaims, - SendMessagePayload(payload):SendMessagePayload + AuthClaims { user_id, hotel_id }: AuthClaims, + SendMessagePayload(payload): SendMessagePayload, ) -> impl IntoResponse { - + //TODO: make sur the convid is valid let pool = state.hotel_pools.get_pool(hotel_id); - let conn = match pool.get(){ + let conn = match pool.get() { Ok(conn) => conn, - Err(err) => return (StatusCode::INTERNAL_SERVER_ERROR, format!("Pool error")) + Err(err) => return (StatusCode::INTERNAL_SERVER_ERROR, format!("Pool error")), }; let mut statement = match conn.prepare( - "SELECT 1 FROM conversation_participants WHERE user_id = ?1 AND conversation_id = ?2" , - ){ + "SELECT 1 FROM conversation_participants WHERE user_id = ?1 AND conversation_id = ?2", + ) { Ok(statement) => statement, - Err(_) => return (StatusCode::INTERNAL_SERVER_ERROR, "prepare failed".to_string()) + Err(_) => { + return ( + StatusCode::INTERNAL_SERVER_ERROR, + "prepare failed".to_string(), + ); + } }; match statement.exists(params![user_id, payload.conv_id]) { - Ok(true) => { - // user is part of the conversation — continue - } - Ok(false) => { - // early exit: not part of the conversation - return (StatusCode::FORBIDDEN, "Not part of the conversation".to_string()); - } - Err(_) => { - // query failed - return (StatusCode::INTERNAL_SERVER_ERROR, "Query failed".to_string()); - } -} - - let result = conn.execute( - "INSERT INTO message (sender_id, content, conversation_id) VALUES (?1, ?2, ?3)", - params![&user_id, &payload.message, &payload.conv_id], - ); - - match result { - Ok(rows) if rows > 0 => { - // --- send to conversation participants --- - let mut stmt_participants = conn - .prepare("SELECT user_id FROM conversation_participants WHERE conversation_id = ?1") - .expect("prepare participants failed"); - - let participant_ids: Vec = stmt_participants - .query_map(params![payload.conv_id], |row| row.get(0)) - .expect("query_map failed") - .filter_map(Result::ok) - .collect(); - - if let Some(hotel_users) = state.ws_map.get(&hotel_id) { - let update_msg = serde_json::json!({ - "event-type": "chat-message", - "conv_id": payload.conv_id, - "sender": user_id, - "content": payload.message, - }) - .to_string(); - - for uid in &participant_ids { - if let Some(sender) = hotel_users.get(&uid) { - let _ = sender.send(axum::extract::ws::Message::Text(update_msg.clone().into())); - } - } - } - - (StatusCode::OK, format!("sent message: {}, to users:{:?}", payload.message, participant_ids)) + Ok(true) => { + // user is part of the conversation — continue + } + Ok(false) => { + // early exit: not part of the conversation + return ( + StatusCode::FORBIDDEN, + "Not part of the conversation".to_string(), + ); + } + Err(_) => { + // query failed + return ( + StatusCode::INTERNAL_SERVER_ERROR, + "Query failed".to_string(), + ); } - Ok(_) => (StatusCode::NOT_FOUND, "Conversation not found".to_string()), - Err(err) => (StatusCode::INTERNAL_SERVER_ERROR, format!("Error from DB: {err}")), } -} + let (message_id, sent_at): (i64, String) = match conn.query_row( + "INSERT INTO message (sender_id, content, conversation_id) + VALUES (?1, ?2, ?3) + RETURNING id, sent_at", + params![user_id, payload.message, payload.conv_id], + |row| Ok((row.get::<_, i64>(0)?, row.get::<_, String>(1)?)), + ) { + Ok(v) => v, + Err(err) => { + return ( + StatusCode::INTERNAL_SERVER_ERROR, + format!("DB insert failed: {err}"), + ); + } + }; + //let message_id = conn.last_insert_rowid(); + // FIXME: add sent_at and message id in the response. + // --- send to conversation participants --- + let mut stmt_participants = conn + .prepare("SELECT user_id FROM conversation_participants WHERE conversation_id = ?1") + .expect("prepare participants failed"); + + let participant_ids: Vec = stmt_participants + .query_map(params![payload.conv_id], |row| row.get(0)) + .expect("query_map failed") + .filter_map(Result::ok) + .collect(); + + if let Some(hotel_users) = state.ws_map.get(&hotel_id) { + let update_msg = serde_json::json!({ + "event_type": "chat_message", + "conv_id": payload.conv_id, + "sender_id": user_id, + "content": payload.message, + "id": message_id, + "sent_at": sent_at, + }) + .to_string(); + + for uid in &participant_ids { + if let Some(sender) = hotel_users.get(uid) { + let _ = sender.send(axum::extract::ws::Message::Text(update_msg.clone().into())); + } + } + } + + ( + StatusCode::OK, + format!( + "sent message: {}, to users:{:?}", + payload.message, participant_ids + ), + ) +} +//Ok(_) => (StatusCode::NOT_FOUND, "Conversation not found".to_string()), #[derive(Debug, Serialize)] struct Message { @@ -187,16 +380,15 @@ struct Message { sent_at: String, } - pub async fn get_message( State(state): State, - AuthClaims {user_id, hotel_id}: AuthClaims, - GetMessagesPayload(payload):GetMessagesPayload + AuthClaims { user_id, hotel_id }: AuthClaims, + GetMessagesPayload(payload): GetMessagesPayload, ) -> Result { - let pool = state.hotel_pools.get_pool(hotel_id); - let conn = pool.get() + let conn = pool + .get() .map_err(|_| (StatusCode::INTERNAL_SERVER_ERROR, "Pool error".to_string()))?; let from_time = match payload.timestamp.as_deref() { @@ -204,82 +396,115 @@ pub async fn get_message( Some(ts) => ts, }; - let mut stmt = conn.prepare( - "SELECT id, sender_id, content, sent_at + let mut stmt = conn + .prepare( + "SELECT id, sender_id, content, sent_at FROM message WHERE conversation_id = ?1 AND sent_at > ?2 ORDER BY sent_at DESC - LIMIT 50" - ).map_err(|_| (StatusCode::INTERNAL_SERVER_ERROR, "Prepare failed".to_string()))?; + LIMIT 50", + ) + .map_err(|_| { + ( + StatusCode::INTERNAL_SERVER_ERROR, + "Prepare failed".to_string(), + ) + })?; - - let messages = stmt.query_map( - params![payload.conv_id, from_time], - |row| { + let messages = stmt + .query_map(params![payload.conv_id, from_time], |row| { Ok(Message { id: row.get(0)?, sender_id: row.get(1)?, content: row.get(2)?, sent_at: row.get(3)?, }) - } - ) - .map_err(|_| (StatusCode::INTERNAL_SERVER_ERROR, "Query failed".to_string()))? - .collect::, _>>() - .map_err(|_| (StatusCode::INTERNAL_SERVER_ERROR, "Collect failed".to_string()))?; + }) + .map_err(|_| { + ( + StatusCode::INTERNAL_SERVER_ERROR, + "Query failed".to_string(), + ) + })? + .collect::, _>>() + .map_err(|_| { + ( + StatusCode::INTERNAL_SERVER_ERROR, + "Collect failed".to_string(), + ) + })?; let response = serde_json::to_string(&messages) .map_err(|_| (StatusCode::INTERNAL_SERVER_ERROR, "Serialisation failed")); - Ok((StatusCode::OK, response )) - + Ok((StatusCode::OK, response)) } - #[derive(Serialize)] struct User { id: i32, username: String, - display_name: String, + //display_name: String, } - pub async fn get_hotel_users( State(state): State, AuthClaims { hotel_id, .. }: AuthClaims, ) -> impl IntoResponse { let conn = match state.logs_pool.get() { Ok(c) => c, - Err(_) => return (StatusCode::INTERNAL_SERVER_ERROR, "DB connection error".to_string()), + Err(_) => { + return ( + StatusCode::INTERNAL_SERVER_ERROR, + "DB connection error".to_string(), + ); + } }; - let mut stmt = match conn.prepare( - "SELECT id, username, displayname FROM users WHERE hotel_id = ?1", - ) { - Ok(s) => s, - Err(e) => return (StatusCode::INTERNAL_SERVER_ERROR, format!("Prepare failed: {}", e)), - }; + let mut stmt = + match conn.prepare("SELECT user_id, username FROM hotel_user_link WHERE hotel_id = ?1") { + Ok(s) => s, + Err(e) => { + return ( + StatusCode::INTERNAL_SERVER_ERROR, + format!("Prepare failed: {}", e), + ); + } + }; let users_iter = match stmt.query_map(params![hotel_id], |row| { Ok(User { id: row.get(0)?, username: row.get(1)?, - display_name: row.get(2)?, + //display_name: row.get(2)?, }) }) { Ok(iter) => iter, - Err(e) => return (StatusCode::INTERNAL_SERVER_ERROR, format!("Query failed: {}", e)), + Err(e) => { + return ( + StatusCode::INTERNAL_SERVER_ERROR, + format!("Query failed: {}", e), + ); + } }; let users: Vec = match users_iter.collect::, _>>() { Ok(u) => u, - Err(e) => return (StatusCode::INTERNAL_SERVER_ERROR, format!("Collect failed: {}", e)), + Err(e) => { + return ( + StatusCode::INTERNAL_SERVER_ERROR, + format!("Collect failed: {}", e), + ); + } }; match serde_json::to_string(&users) { Ok(json) => (StatusCode::OK, json), - Err(e) => (StatusCode::INTERNAL_SERVER_ERROR, format!("Serialization failed: {}", e)), + Err(e) => ( + StatusCode::INTERNAL_SERVER_ERROR, + format!("Serialization failed: {}", e), + ), } } @@ -289,72 +514,75 @@ struct Conversation { title: String, } +//FIXME: allow null conv name ? default to persons name pub async fn get_convs( State(state): State, //Path((item_name, item_amount)): Path<(String, i32)>, - AuthClaims{ user_id, hotel_id}: AuthClaims, + AuthClaims { user_id, hotel_id }: AuthClaims, ) -> impl IntoResponse { - let pool = state.hotel_pools.get_pool(hotel_id); - let conn = match pool.get(){ + let conn = match pool.get() { Ok(conn) => conn, Err(err) => { - let body = json!({ "error": format!("Pool error: {}", err) }); - return (StatusCode::INTERNAL_SERVER_ERROR, Json(body)); + return ( + StatusCode::INTERNAL_SERVER_ERROR, + format!("Pool error: {err}"), + ); } }; - let mut stmt = match conn.prepare( - "SELECT conversation_id, name FROM conversation_participants WHERE user_id = ?1", - ) { + let mut stmt = match conn + .prepare("SELECT conversation_id, name FROM conversation_participants WHERE user_id = ?1") + { Ok(s) => s, Err(e) => { - let body = json!({ "error": format!("Prepare failed: {}", e) }); - return (StatusCode::INTERNAL_SERVER_ERROR, Json(body) ) - + return ( + StatusCode::INTERNAL_SERVER_ERROR, + format!("Prepare failed: {}", e), + ); } }; let rows = match stmt.query_map(params![user_id], |row| { let conversation_id: i32 = row.get(0)?; let name: String = row.get(1)?; - Ok((conversation_id, name)) + Ok(Conversation { + id: row.get(0)?, + title: row.get(1)?, + }) }) { Ok(rows) => rows, //Ok(_) => {}, IMPLEMENT NO CONV ? Err(e) => { - let body = json!({ "error": format!("Query failed: {}", e) }); - return (StatusCode::INTERNAL_SERVER_ERROR, Json(body)); + return ( + StatusCode::INTERNAL_SERVER_ERROR, + format!("Query failed: {}", e), + ); } }; - - let mut map = HashMap::new(); - - // ✅ Iterate through the row results - for row_result in rows { - match row_result { - Ok((id, name)) => { - map.insert(id, name); - } - Err(e) => { - let body = json!({ "error": format!("Row parsing failed: {}", e) }); - return (StatusCode::INTERNAL_SERVER_ERROR, Json(body)); - } + let convs: Vec = match rows.collect::, _>>() { + Ok(u) => u, + Err(e) => { + return ( + StatusCode::INTERNAL_SERVER_ERROR, + format!("Collect failed: {}", e), + ); } + }; + + //.map_err(|_| (StatusCode::INTERNAL_SERVER_ERROR, Json("error".to_string()))); + + match serde_json::to_string(&convs) { + Ok(json) => (StatusCode::OK, json), + Err(e) => ( + StatusCode::INTERNAL_SERVER_ERROR, + format!("Serialization failed: {}", e), + ), } - - let convs_string = serde_json::to_string(&map) - .map_err(|_| (StatusCode::INTERNAL_SERVER_ERROR, Json("error".to_string()))); - - let conv_map_json = to_value(map).unwrap(); - (StatusCode::OK, Json(conv_map_json)) } - - - /* pub async fn get_convs( State(state): State, @@ -367,18 +595,18 @@ pub async fn get_convs( let conn = match pool.get(){ Ok(conn) => conn, Err(err) => return (StatusCode::INTERNAL_SERVER_ERROR, format!("Pool error: {}", err).into_response() ) - + }; let mut stmt = match conn.prepare( "SELECT id, title FROM conversation WHERE creator_id = ?1", ) { Ok(s) => s, - Err(e) => - + Err(e) => + return (StatusCode::INTERNAL_SERVER_ERROR, format!("Prepare failed: {}", e).into_response() ) - - + + }; let rows = match stmt.query_map(params![user_id], |row| { @@ -388,10 +616,10 @@ pub async fn get_convs( }) { Ok(rows) => rows, Err(e) => return (StatusCode::INTERNAL_SERVER_ERROR, format!("Query failed: {}", e).into_response() ) - + }; - + let mut map = HashMap::new(); // ✅ Iterate through the row results @@ -401,7 +629,7 @@ pub async fn get_convs( map.insert(title, id); } Err(e) => return (StatusCode::INTERNAL_SERVER_ERROR, format!("Row parsing failed: {}", e).into_response() ) - + } } @@ -410,7 +638,7 @@ pub async fn get_convs( Ok(c) => c, Err(e) => return (StatusCode::INTERNAL_SERVER_ERROR, format!("List unwrapping failed: {}", e).into_response() ) }; - + let conv_map_clean_json = serde_json::to_value(map) .map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, format!("Serialization failed: {}", e).into_response() )); @@ -419,4 +647,4 @@ pub async fn get_convs( (StatusCode::OK, Json(conv_map_clean_json)).into_response() } -*/ \ No newline at end of file +*/ diff --git a/src/chat/mod.rs b/src/chat/mod.rs index 4f1dd12..2b6ada4 100644 --- a/src/chat/mod.rs +++ b/src/chat/mod.rs @@ -1,5 +1,4 @@ pub mod routes; - mod extractor; -mod handlers; \ No newline at end of file +mod handlers; diff --git a/src/chat/routes.rs b/src/chat/routes.rs index aeb6edf..04317b3 100644 --- a/src/chat/routes.rs +++ b/src/chat/routes.rs @@ -1,25 +1,15 @@ -use axum::{ - Router, - routing::put, - routing::post, - routing::get, -}; +use axum::{Router, routing::get, routing::post, routing::put}; -use crate::utils::db_pool::AppState; use crate::chat::handlers::*; - - - - +use crate::utils::db_pool::AppState; pub fn chat_routes() -> Router { - Router::new() - .route("/create_conversation", post (create_conversation)) + .route("/create_conversation", post(create_conversation)) .route("/add_users_conv", put(add_user_to_conv)) .route("/send_message", post(send_message)) - .route("/get_conv", get(get_convs)) - .route("/get_message", get(get_message)) - .route("/hotel_users", get(get_hotel_users)) - - } \ No newline at end of file + .route("/get_conv", post(get_convs)) + .route("/get_message", post(get_message)) + .route("/hotel_users", post(get_hotel_users)) + .route("/get_conv_users/{conv_id}", post(get_conv_users)) +} diff --git a/src/inventory/handler.rs b/src/inventory/handler.rs index 86b6365..b0227be 100644 --- a/src/inventory/handler.rs +++ b/src/inventory/handler.rs @@ -1,36 +1,48 @@ - use argon2::Params; -use axum::{extract::{ws::{close_code::STATUS, Message}, Path, State}, http::StatusCode, response::IntoResponse}; +use axum::{ + extract::{ + Path, State, + ws::{Message, close_code::STATUS}, + }, + http::StatusCode, + response::IntoResponse, +}; use rusqlite::params; use serde::Serialize; use serde_json::json; - - use crate::utils::{auth::AuthClaims, db_pool::AppState}; - - pub async fn create_inventory_item( State(state): State, Path((item_name, item_amount)): Path<(String, i32)>, - AuthClaims{ user_id, hotel_id}: AuthClaims, + AuthClaims { user_id, hotel_id }: AuthClaims, ) -> impl IntoResponse { - let pool = state.hotel_pools.get_pool(hotel_id); - let conn = match pool.get(){ + let conn = match pool.get() { Ok(conn) => conn, - Err(err) => return (StatusCode::INTERNAL_SERVER_ERROR, format!("couldn't open the connection")) + Err(err) => { + return ( + StatusCode::INTERNAL_SERVER_ERROR, + format!("couldn't open the connection"), + ); + } }; let result = conn.execute( "INSERT INTO inventory (item_name, amount, user_id) VALUES (?1, ?2, ?3)", - params![&item_name,&item_amount,&user_id] + params![&item_name, &item_amount, &user_id], ); match result { - Ok(rows) => (StatusCode::OK, format!("inserted item {item_name}, with {item_amount} amount")), - Err(err) => (StatusCode::INTERNAL_SERVER_ERROR, format!("couldn't add the new item, err: {}", err )) + Ok(rows) => ( + StatusCode::OK, + format!("inserted item {item_name}, with {item_amount} amount"), + ), + Err(err) => ( + StatusCode::INTERNAL_SERVER_ERROR, + format!("couldn't add the new item, err: {}", err), + ), } } @@ -39,7 +51,7 @@ pub struct InventoryItems { id: i32, amount: i32, name: String, - user_id: i32, + user_id: i32, updated_at: String, } @@ -48,16 +60,20 @@ pub async fn update_inventory_item( Path((item_id, item_amount)): Path<(i32, i32)>, AuthClaims { user_id, hotel_id }: AuthClaims, ) -> impl IntoResponse { - //TODO: make better error handling : - // if wrong param collumn targeted, - // if missing path param + // if wrong param collumn targeted, + // if missing path param let pool = state.hotel_pools.get_pool(hotel_id); - - let conn = match pool.get(){ + + let conn = match pool.get() { Ok(conn) => conn, - Err(err) => return (StatusCode::INTERNAL_SERVER_ERROR, format!("Pool error: {err}")), + Err(err) => { + return ( + StatusCode::INTERNAL_SERVER_ERROR, + format!("Pool error: {err}"), + ); + } }; let result: Result = conn.query_row( @@ -68,31 +84,51 @@ pub async fn update_inventory_item( match result { Ok(item_name) => { - if let Err(err) = conn.execute( + if let Err(err) = conn.execute( "INSERT INTO inventory_history (item_id, amount, item_name, user_id) VALUES (?1,?2,?3,?4)", params![&item_id,&item_amount,&item_name, &user_id] ){ return (StatusCode::INTERNAL_SERVER_ERROR, format!("failed to update inventory history")); } + + if let Some(hotel_users) = state.ws_map.get(&hotel_id) { + let update_msg = json!({ + "event_type": "item_update", + "item_id": item_id, + "number": item_amount, + "updated_by": user_id, + }) + .to_string(); + + for entry in hotel_users.iter() { + let sender = entry.value(); + // ignore errors (client disconnected) + let _ = sender.send(Message::Text(update_msg.clone().into())); + } + } + (StatusCode::OK, format!("updated item history")) } - - Ok(_) => (StatusCode::NOT_FOUND, "No room found".to_string()), - Err(err) => (StatusCode::INTERNAL_SERVER_ERROR, format!("Error from DB: {err}")), - + + Ok(_) => ( + StatusCode::NOT_FOUND, + "No item found, err : {_}".to_string(), + ), + Err(err) => ( + StatusCode::INTERNAL_SERVER_ERROR, + format!("Error from DB: {err}"), + ), } -/* - match result { - Ok(row) => (StatusCode::OK, format!("Items updated")), - Ok(_) => (StatusCode::NOT_FOUND, format!("No item with this id exist")), - Err(err) => (StatusCode::INTERNAL_SERVER_ERROR, format!("error updating the item with id :{} with amount: {}", item_id, item_amount)) - } -*/ - + /* + match result { + Ok(row) => (StatusCode::OK, format!("Items updated")), + Ok(_) => (StatusCode::NOT_FOUND, format!("No item with this id exist")), + Err(err) => (StatusCode::INTERNAL_SERVER_ERROR, format!("error updating the item with id :{} with amount: {}", item_id, item_amount)) + } + */ } - pub async fn get_inventory_item( State(state): State, AuthClaims { user_id, hotel_id }: AuthClaims, @@ -104,10 +140,16 @@ pub async fn get_inventory_item( Err(_) => return (StatusCode::INTERNAL_SERVER_ERROR, "Pool error".to_string()), }; - let mut stmt = match conn.prepare("SELECT id, amount, item_name, user_id FROM inventory") { - Ok(s) => s, - Err(_) => return (StatusCode::INTERNAL_SERVER_ERROR, "Statement error".to_string()), - }; + let mut stmt = + match conn.prepare("SELECT id, amount, item_name, user_id, updated_at FROM inventory") { + Ok(s) => s, + Err(_) => { + return ( + StatusCode::INTERNAL_SERVER_ERROR, + "Statement error".to_string(), + ); + } + }; let mut query_result = match stmt.query([]) { Ok(r) => r, @@ -120,7 +162,7 @@ pub async fn get_inventory_item( let item = InventoryItems { id: row.get("id").unwrap_or_default(), amount: row.get("amount").unwrap_or_default(), - name: row.get("name").unwrap_or_default(), + name: row.get("item_name").unwrap_or_default(), user_id: row.get("user_id").unwrap_or_default(), updated_at: row.get("updated_at").unwrap_or_default(), }; @@ -130,8 +172,13 @@ pub async fn get_inventory_item( // Serialize to JSON let json = match serde_json::to_string(&items) { Ok(j) => j, - Err(_) => return (StatusCode::INTERNAL_SERVER_ERROR, "Serialization error".to_string()), + Err(_) => { + return ( + StatusCode::INTERNAL_SERVER_ERROR, + "Serialization error".to_string(), + ); + } }; (StatusCode::OK, json) -} \ No newline at end of file +} diff --git a/src/inventory/mod.rs b/src/inventory/mod.rs index 5a539d5..32e4f70 100644 --- a/src/inventory/mod.rs +++ b/src/inventory/mod.rs @@ -1,3 +1,3 @@ mod handler; -pub mod routes; \ No newline at end of file +pub mod routes; diff --git a/src/inventory/routes.rs b/src/inventory/routes.rs index b5d2b96..70adc3b 100644 --- a/src/inventory/routes.rs +++ b/src/inventory/routes.rs @@ -1,14 +1,19 @@ use axum::{ - routing::{get, put, post}, Router, + routing::{get, post, put}, }; use crate::{inventory::handler::*, utils::db_pool::AppState}; pub fn inventory_routes() -> Router { - Router::new() - .route("/update_item/{item_id}/{item_amount}", put(update_inventory_item)) - .route("/add_item/{item_name}/{item_amount}", post(create_inventory_item)) + .route( + "/update_item/{item_id}/{item_amount}", + put(update_inventory_item), + ) + .route( + "/add_item/{item_name}/{item_amount}", + post(create_inventory_item), + ) .route("/get_item/", get(get_inventory_item)) -} \ No newline at end of file +} diff --git a/src/lib.rs b/src/lib.rs index 0128a59..b19af3c 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,8 +1,7 @@ -#[allow(unused_imports)] -#[allow(dead_code)] - -pub mod utils; -pub mod routes; -pub mod rooms; pub mod chat; pub mod inventory; +pub mod rooms; +pub mod routes; +#[allow(unused_imports)] +#[allow(dead_code)] +pub mod utils; diff --git a/src/main.rs b/src/main.rs index 501c9e4..8df2ca1 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,30 +1,37 @@ -use axum::serve; use axum::Extension; -use axum::extract::{ws::{Message, WebSocket, WebSocketUpgrade}, State}; +use axum::extract::{ + State, + ws::{Message, WebSocket, WebSocketUpgrade}, +}; +use axum::serve; use jsonwebtoken::{DecodingKey, EncodingKey}; +use reqwest::header::AUTHORIZATION; +use reqwest::header::CONTENT_TYPE; +use reqwest::header::USER_AGENT; + + use tokio::net::TcpListener; -use tokio::sync::mpsc; -use reqwest::Client; +use crate::utils::auth::JwtKeys; +use crate::utils::db_pool::{AppState, HotelPool}; +use routes::create_router; + +use dotenvy::dotenv; +use std::env; +//use tower_http::cors::Origin; +use axum::http::{HeaderValue, Method}; +use tower_http::cors::{Any, CorsLayer}; -mod utils; -mod routes; -mod rooms; mod chat; mod inventory; -use r2d2::{Pool}; -use r2d2_sqlite::SqliteConnectionManager; +mod rooms; +mod routes; +mod utils; use dashmap::DashMap; +use r2d2::Pool; +use r2d2_sqlite::SqliteConnectionManager; use std::sync::Arc; -use crate::utils::db_pool::{HotelPool,AppState}; -use routes::create_router; -use crate::utils::auth::JwtKeys; - -use std::env; -use dotenvy::dotenv; - - pub async fn notify_discord(msg: &str) -> Result<(), reqwest::Error> { let payload = serde_json::json!({ "content": msg @@ -39,31 +46,29 @@ pub async fn notify_discord(msg: &str) -> Result<(), reqwest::Error> { Ok(()) } - #[tokio::main(flavor = "multi_thread", worker_threads = 8)] async fn main() -> std::io::Result<()> { - dotenv().ok(); -std::panic::set_hook(Box::new(|info| { - let msg = format!("Rust panic: {}", info); + std::panic::set_hook(Box::new(|info| { + let msg = format!("Rust panic: {}", info); - // Use blocking client so the process can't exit before sending - let payload = serde_json::json!({ - "content": msg - }); + // Use blocking client so the process can't exit before sending + let payload = serde_json::json!({ + "content": msg + }); - let client = reqwest::blocking::Client::new(); - let _ = client - .post("https://discord.com/api/webhooks/1440912618205347891/Ekg89krDoPm41kA27LA3gXgNWmMWvCCtziYIUsjqaY22Jnw4a6IWhZOht0in5JjnPX-W") - .json(&payload) - .send(); -})); + let client = reqwest::blocking::Client::new(); + let _ = client + .post("https://discord.com/api/webhooks/1440912618205347891/Ekg89krDoPm41kA27LA3gXgNWmMWvCCtziYIUsjqaY22Jnw4a6IWhZOht0in5JjnPX-W") + .json(&payload) + .send(); + })); -//panic!("crash-test"); + //panic!("crash-test"); let hotel_pools = HotelPool::new(); - let logs_manager = SqliteConnectionManager::file("db/auth.sqlite"); + let logs_manager = SqliteConnectionManager::file("db/auth_copy_2.sqlite"); let logs_pool = Pool::builder() .max_size(5) .build(logs_manager) @@ -72,31 +77,34 @@ std::panic::set_hook(Box::new(|info| { let state = AppState { hotel_pools, logs_pool, - ws_map: Arc::new(DashMap::new()), + ws_map: Arc::new(DashMap::new()), //jwt_secret: "your_jwt_secret_key s".to_string(), // better: load from env var }; //let jwt_secret = "your_jwt_secret_key".to_string(); - + let jwt_secret = env::var("JWT_SECRET") .expect("JWT_SECRET must be set") .to_string(); - + let jwt_keys = JwtKeys { encoding: EncodingKey::from_secret(jwt_secret.as_ref()), decoding: DecodingKey::from_secret(jwt_secret.as_ref()), }; + let allowed_origins = vec!["http://82.66.253.209", "http://localhost:5173"]; + let cors = CorsLayer::very_permissive() + .allow_credentials(true) + .allow_methods([Method::GET, Method::POST, Method::PUT, Method::OPTIONS]) + .allow_headers([CONTENT_TYPE, AUTHORIZATION, USER_AGENT]); + let app = create_router(state).layer(Extension(jwt_keys)).layer(cors); - let app = create_router(state) - .layer(Extension(jwt_keys)); - - let listener = TcpListener::bind("0.0.0.0:8080").await?; + let listener = TcpListener::bind("0.0.0.0:7080").await?; serve(listener, app).into_future().await?; Ok(()) } async fn handler() -> &'static str { "Hiii from localhost" -} \ No newline at end of file +} diff --git a/src/note_endpoint_json.txt b/src/note_endpoint_json.txt deleted file mode 100644 index 73acf09..0000000 --- a/src/note_endpoint_json.txt +++ /dev/null @@ -1,51 +0,0 @@ -#[derive(Deserialize, Debug)] -pub struct GetMessagesValues { - pub conv_id: u32, - pub timestamp: Option, -} - - -pub async fn get_message( - State(state): State, - AuthClaims { user_id, hotel_id, username }: AuthClaims, - Json(payload): Json, // ✅ no custom payload wrapper -) -> Result { - - let pool = state.hotel_pools.get_pool(hotel_id); - - let conn = pool.get() - .map_err(|_| (StatusCode::INTERNAL_SERVER_ERROR, "Pool error".to_string()))?; - - let from_time = match payload.timestamp.as_deref() { - Some("0") | None => "1970-01-01 00:00:00", - Some(ts) => ts, - }; - - let mut stmt = conn.prepare( - "SELECT id, sender_id, content, sent_at - FROM message - WHERE conversation_id = ?1 - AND sent_at > ?2 - ORDER BY sent_at DESC - LIMIT 50" - ).map_err(|_| (StatusCode::INTERNAL_SERVER_ERROR, "Prepare failed".to_string()))?; - - let messages = stmt.query_map( - params![payload.conv_id, from_time], - |row| { - Ok(Message { - id: row.get(0)?, - sender_id: row.get(1)?, - content: row.get(2)?, - sent_at: row.get(3)?, - }) - } - ) - .map_err(|_| (StatusCode::INTERNAL_SERVER_ERROR, "Query failed".to_string()))? - .collect::, _>>() - .map_err(|_| (StatusCode::INTERNAL_SERVER_ERROR, "Collect failed".to_string()))?; - - -} - - diff --git a/src/rooms/extractor.rs b/src/rooms/extractor.rs index d068ff9..f045e05 100644 --- a/src/rooms/extractor.rs +++ b/src/rooms/extractor.rs @@ -1,9 +1,8 @@ use axum::{ - extract::{Request, FromRequest, Path}, - body::{Body}, - - http::StatusCode, Json, Router, + body::Body, + extract::{FromRequest, Path, Request}, + http::StatusCode, }; use serde::Deserialize; @@ -16,7 +15,8 @@ pub struct UpdateRoomPayload(pub UpdateRoomValues); //#[async_trait] impl FromRequest for UpdateRoomPayload -where S: Send + Sync, +where + S: Send + Sync, { type Rejection = (StatusCode, String); @@ -24,14 +24,14 @@ where S: Send + Sync, let Json(payload) = Json::::from_request(req, state) .await .map_err(|err| (StatusCode::BAD_REQUEST, format!("Invalid body: {}", err)))?; - + Ok(UpdateRoomPayload(payload)) } } #[derive(Debug)] pub struct RoomIdValue { - pub room_id: i32 + pub room_id: i32, } -pub type RoomIdPath = Path; \ No newline at end of file +pub type RoomIdPath = Path; diff --git a/src/rooms/handler.rs b/src/rooms/handler.rs index ff7e963..6341130 100644 --- a/src/rooms/handler.rs +++ b/src/rooms/handler.rs @@ -1,22 +1,20 @@ -use axum::{Json, extract::Path, extract::State }; -use axum::response::IntoResponse; -use axum::http::StatusCode; use axum::extract::ws::Message; +use axum::http::StatusCode; +use axum::response::IntoResponse; +use axum::{Json, extract::Path, extract::State}; use serde::Serialize; use serde_json::json; use crate::rooms::extractor::UpdateRoomPayload; //use crate::utils::db_pool::*; use crate::utils::auth::AuthClaims; -use crate::utils::db_pool::{HotelPool,AppState}; +use crate::utils::db_pool::{AppState, HotelPool}; -use std::sync::Arc; -use r2d2::{Pool}; -use r2d2_sqlite::SqliteConnectionManager; use dashmap::DashMap; +use r2d2::Pool; +use r2d2_sqlite::SqliteConnectionManager; use rusqlite::params; - - +use std::sync::Arc; pub async fn hello_rooms() -> String { "hello from rooms".to_string() @@ -28,11 +26,15 @@ pub async fn fake_db_update( Path(room_id): Path, UpdateRoomPayload(payload): UpdateRoomPayload, ) -> impl IntoResponse { - let pool = state.hotel_pools.get_pool(hotel_id); let conn = match pool.get() { Ok(conn) => conn, - Err(err) => return (StatusCode::INTERNAL_SERVER_ERROR, format!("Pool error: {err}")), + Err(err) => { + return ( + StatusCode::INTERNAL_SERVER_ERROR, + format!("Pool error: {err}"), + ); + } }; let result = conn.execute( @@ -41,9 +43,15 @@ pub async fn fake_db_update( ); match result { - Ok(rows) if rows > 0 => (StatusCode::OK, format!("Updated room {room_id} in hotel {}", hotel_id)), + Ok(rows) if rows > 0 => ( + StatusCode::OK, + format!("Updated room {room_id} in hotel {}", hotel_id), + ), Ok(_) => (StatusCode::NOT_FOUND, "No room found".to_string()), - Err(err) => (StatusCode::INTERNAL_SERVER_ERROR, format!("DB error: {err}")), + Err(err) => ( + StatusCode::INTERNAL_SERVER_ERROR, + format!("DB error: {err}"), + ), } } @@ -53,16 +61,20 @@ pub async fn clean_db_update( AuthClaims { user_id, hotel_id }: AuthClaims, UpdateRoomPayload(payload): UpdateRoomPayload, ) -> impl IntoResponse { - //TODO: make better error handling : - // if wrong param collumn targeted, - // if missing path param + // if wrong param collumn targeted, + // if missing path param let pool = state.hotel_pools.get_pool(hotel_id); - - let conn = match pool.get(){ + + let conn = match pool.get() { Ok(conn) => conn, - Err(err) => return (StatusCode::INTERNAL_SERVER_ERROR, format!("Pool error: {err}")), + Err(err) => { + return ( + StatusCode::INTERNAL_SERVER_ERROR, + format!("Pool error: {err}"), + ); + } }; let result: Result = conn.query_row( @@ -71,7 +83,6 @@ pub async fn clean_db_update( |row| row.get(0), ); - match result { Ok(room_number) => { // --- broadcast to all WS clients in the hotel --- @@ -79,11 +90,14 @@ pub async fn clean_db_update( "INSERT INTO room_history (room_id, room_number, status) VALUES (?1, ?2, ?3)", params![&room_id, &room_number, &payload.status], ) { - return (StatusCode::INTERNAL_SERVER_ERROR, format!("Failed to insert history: {err}")); + return ( + StatusCode::INTERNAL_SERVER_ERROR, + format!("Failed to insert history: {err}"), + ); } if let Some(hotel_users) = state.ws_map.get(&hotel_id) { let update_msg = json!({ - "event-type": "room-update", + "event_type": "room_update", "room_id": room_id, "status": payload.status, "updated_by": user_id, @@ -97,10 +111,19 @@ pub async fn clean_db_update( } } - (StatusCode::OK, format!("updated room {room_id} in hotel {hotel_id}, with status: {}", payload.status)) + ( + StatusCode::OK, + format!( + "updated room {room_id} in hotel {hotel_id}, with status: {}", + payload.status + ), + ) } Ok(_) => (StatusCode::NOT_FOUND, "No room found".to_string()), - Err(err) => (StatusCode::INTERNAL_SERVER_ERROR, format!("Error from DB: {err}")), + Err(err) => ( + StatusCode::INTERNAL_SERVER_ERROR, + format!("Error from DB: {err}"), + ), } } @@ -115,50 +138,65 @@ pub async fn get_all_rooms( State(state): State, AuthClaims { hotel_id, .. }: AuthClaims, ) -> impl IntoResponse { - let pool = state.hotel_pools.get_pool(hotel_id); - - let conn = match pool.get(){ + + let conn = match pool.get() { Ok(conn) => conn, - Err(err) => return (StatusCode::INTERNAL_SERVER_ERROR, format!("Pool error: {err}")), - }; - - let mut stmt = match conn.prepare( - "SELECT id, number, status FROM rooms ", - ) { - Ok(s) => s, - Err(e) => return (StatusCode::INTERNAL_SERVER_ERROR, format!("Prepare failed: {}", e)), - }; - - let room_iter = match stmt.query_map( - params![],|row| { - Ok(Room { - id: row.get(0)?, - number: row.get(1)?, - status: row.get(2)?, - }) + Err(err) => { + return ( + StatusCode::INTERNAL_SERVER_ERROR, + format!("Pool error: {err}"), + ); } - ) { + }; + + let mut stmt = match conn.prepare("SELECT id, number, status FROM rooms ") { + Ok(s) => s, + Err(e) => { + return ( + StatusCode::INTERNAL_SERVER_ERROR, + format!("Prepare failed: {}", e), + ); + } + }; + + let room_iter = match stmt.query_map(params![], |row| { + Ok(Room { + id: row.get(0)?, + number: row.get(1)?, + status: row.get(2)?, + }) + }) { Ok(iter) => iter, - Err(e) => return (StatusCode::INTERNAL_SERVER_ERROR, format!("Query failed: {}", e)), + Err(e) => { + return ( + StatusCode::INTERNAL_SERVER_ERROR, + format!("Query failed: {}", e), + ); + } }; let rooms: Vec = match room_iter.collect::, _>>() { Ok(u) => u, - Err(e) => return (StatusCode::INTERNAL_SERVER_ERROR, format!("Collect failed: {}", e)), + Err(e) => { + return ( + StatusCode::INTERNAL_SERVER_ERROR, + format!("Collect failed: {}", e), + ); + } }; match serde_json::to_string(&rooms) { Ok(json) => (StatusCode::OK, json), - Err(e) => (StatusCode::INTERNAL_SERVER_ERROR, format!("Serialization failed: {}", e)), + Err(e) => ( + StatusCode::INTERNAL_SERVER_ERROR, + format!("Serialization failed: {}", e), + ), } } - - struct RoomRequest { item_id: i32, item_amount: i32, token: String, } - diff --git a/src/rooms/mod.rs b/src/rooms/mod.rs index ecc5a11..4ac15c9 100644 --- a/src/rooms/mod.rs +++ b/src/rooms/mod.rs @@ -1,6 +1,6 @@ //pub mod handler; //pub mod routes; -mod handler; mod extractor; +mod handler; pub mod routes; diff --git a/src/rooms/routes.rs b/src/rooms/routes.rs index caa2ea9..41522b0 100644 --- a/src/rooms/routes.rs +++ b/src/rooms/routes.rs @@ -1,21 +1,15 @@ use axum::{ - routing::{get, put,}, Router, + routing::{get, put}, }; use crate::rooms::handler::*; -use crate::utils::db_pool::{ - HotelPool, - AppState, -}; - - +use crate::utils::db_pool::{AppState, HotelPool}; // ROOTS pub fn rooms_routes() -> Router { - Router::new() - .route("/", get(hello_rooms) ) + .route("/", get(hello_rooms)) .route("/clean_db_update/{room_id}", put(clean_db_update)) .route("/rooms", get(get_all_rooms)) - } \ No newline at end of file +} diff --git a/src/routes/mod.rs b/src/routes/mod.rs index 61988f2..9bdef9d 100644 --- a/src/routes/mod.rs +++ b/src/routes/mod.rs @@ -1,31 +1,23 @@ -use axum::{ - Router, -}; +use axum::Router; +use r2d2::Pool; use r2d2_sqlite::SqliteConnectionManager; -use r2d2::{Pool}; -use crate::rooms::routes::rooms_routes; -use crate::utils::routes::utils_routes; use crate::chat::routes::chat_routes; use crate::inventory::routes::inventory_routes; +use crate::rooms::routes::rooms_routes; +use crate::utils::routes::utils_routes; -use crate::utils::db_pool::{AppState}; -//TODO: add secret fomr dotenv here +use crate::utils::db_pool::AppState; +//TODO: add secret fomr dotenv here /* -Function to build our main router +Function to build our main router that regroup all feature centered router */ pub fn create_router(state: AppState) -> Router { - Router::new() - .nest("/auth", utils_routes().with_state(state.clone())) .nest("/rooms", rooms_routes().with_state(state.clone())) .nest("/chat", chat_routes().with_state(state.clone())) .nest("/inventory", inventory_routes().with_state(state.clone())) - .with_state(state) - } - - diff --git a/src/utils/auth.rs b/src/utils/auth.rs index b529381..329418b 100644 --- a/src/utils/auth.rs +++ b/src/utils/auth.rs @@ -1,33 +1,45 @@ -use std::time::Duration; use axum::{ - body::{to_bytes, Body}, extract::{Extension, FromRequest, FromRequestParts, Path, State}, http::{header::{HeaderValue, SET_COOKIE}, request::Parts, Request as HttpRequest, StatusCode }, middleware::Next, response::{IntoResponse, IntoResponseParts, Response}, Json + Json, + body::{Body, to_bytes}, + extract::{Extension, FromRequest, FromRequestParts, Path, State, ws::close_code::STATUS}, + http::{ + Request as HttpRequest, StatusCode, + header::{HeaderValue, SET_COOKIE}, + request::Parts, + status, + }, + middleware::Next, + response::{IntoResponse, IntoResponseParts, Response}, }; +use std::time::Duration; use axum_extra::extract::TypedHeader; //use axum_extra::TypedHeader; -use headers::UserAgent; +use futures_util::future::TrySelect; +use headers::{Cookie, UserAgent}; use axum::extract::FromRef; use axum::extract::Request as ExtractRequest; -use jsonwebtoken::{decode, DecodingKey, Validation, encode, EncodingKey, Header, Algorithm}; +use chrono::{Utc, format}; +use jsonwebtoken::{Algorithm, DecodingKey, EncodingKey, Header, Validation, decode, encode}; +use reqwest::header::REFRESH; +use rusqlite::{Connection, OptionalExtension, params}; use serde::{Deserialize, Serialize}; use serde_json::Value; -use chrono::{Utc}; -use rusqlite::{params, Connection, OptionalExtension}; -use rand_core::{RngCore, OsRng}; use argon2::{ + Argon2, password_hash::{PasswordHash, PasswordHasher, PasswordVerifier, SaltString}, - Argon2}; +}; +use rand_core::{OsRng, RngCore}; use uuid::Uuid; - //use crate::utils::db_pool::; -use crate::utils::db_pool::{HotelPool,AppState}; - -use base64::{engine::general_purpose, Engine as _}; +//use crate::utils::db_pool::; +use crate::utils::db_pool::{AppState, HotelPool}; +use base64::{Engine as _, engine::general_purpose}; #[derive(Clone)] pub struct JwtKeys { @@ -40,10 +52,7 @@ pub async fn token_tester( //Extension(keys): Extension, AuthClaims { user_id, hotel_id }: AuthClaims, ) -> impl IntoResponse { - format!( - "(user_id: {}) from hotel {}", - user_id, hotel_id - ) + format!("(user_id: {}) from hotel {}", user_id, hotel_id) } pub struct AuthUser(pub Claims); //?? @@ -55,44 +64,51 @@ pub struct AuthClaims { //pub username: String, } +pub fn auth_claims_from_token( + token: &str, + keys: &JwtKeys, +) -> Result { + let token_data = decode::(token, &keys.decoding, &Validation::new(Algorithm::HS256)) + .map_err(|_| (StatusCode::UNAUTHORIZED, "Invalid token".into()))?; + + Ok(AuthClaims { + user_id: token_data.claims.id, + hotel_id: token_data.claims.hotel_id, + }) +} + impl FromRequestParts for AuthClaims where S: Send + Sync + 'static, - AppState: Clone + Send + Sync + 'static, AppState: FromRef - { + AppState: FromRef, +{ type Rejection = (StatusCode, String); async fn from_request_parts(parts: &mut Parts, state: &S) -> Result { - // We assume your state has a `jwt_secret` field - let Extension(keys): Extension = - Extension::from_request_parts(parts, state).await.map_err(|_| (StatusCode::UNAUTHORIZED, "Missing keys".to_string()))?; + let Extension(keys): Extension = Extension::from_request_parts(parts, state) + .await + .map_err(|_| (StatusCode::UNAUTHORIZED, "Missing keys".into()))?; - // 1️⃣ Extract the token from the Authorization header let auth_header = parts .headers - .get("Authorization") - .ok_or((StatusCode::UNAUTHORIZED, "Missing Authorization header".to_string()))? + .get(axum::http::header::AUTHORIZATION) + .ok_or(( + StatusCode::UNAUTHORIZED, + "Missing Authorization header".into(), + ))? .to_str() - .map_err(|_| (StatusCode::BAD_REQUEST, "Invalid Authorization header".to_string()))?; + .map_err(|_| { + ( + StatusCode::BAD_REQUEST, + "Invalid Authorization header".into(), + ) + })?; - // Bearer token? let token = auth_header .strip_prefix("Bearer ") - .ok_or((StatusCode::BAD_REQUEST, "Expected Bearer token".to_string()))?; - - // 2️⃣ Decode the token - let token_data = decode::( - token, - &keys.decoding, - &Validation::new(Algorithm::HS256), - ).map_err(|_| (StatusCode::UNAUTHORIZED, "Invalid token".to_string()))?; - - Ok(AuthClaims { - user_id: token_data.claims.id, - hotel_id: token_data.claims.hotel_id, - //username: token_data.claims.username, - }) + .ok_or((StatusCode::BAD_REQUEST, "Expected Bearer token".into()))?; + auth_claims_from_token(token, &keys) } } @@ -111,29 +127,30 @@ fn hash_password(password: &str) -> anyhow::Result { // Verify an incoming password against stored hash fn verify_password(password: &str, stored_hash: &str) -> bool { - let parsed_hash = match PasswordHash::new(&stored_hash) { Ok(hash) => hash, Err(_) => return false, }; Argon2::default() - .verify_password(password.as_bytes(), &parsed_hash).is_ok() - + .verify_password(password.as_bytes(), &parsed_hash) + .is_ok() } #[derive(Deserialize, Debug)] -pub struct RegisterValues{ +pub struct RegisterValues { username: String, password: String, - hotel_id: i32, + #[serde(default)] + hotel_ids: Vec, //-> :Vec!<32>, maybe optionnal ? displayname: String, } pub struct RegisterPayload(pub RegisterValues); impl FromRequest for RegisterPayload -where S: Send + Sync, +where + S: Send + Sync, { type Rejection = (StatusCode, String); @@ -145,28 +162,82 @@ where S: Send + Sync, } } -pub async fn register_user ( +//TODO: Validate all hotel_ids first + Use a transaction + Batch query hotel names with IN (...) + +pub async fn register_user( State(state): State, - RegisterPayload(payload): RegisterPayload -) -> Result { + RegisterPayload(payload): RegisterPayload, +) -> Result { + let hashed_password = hash_password(&payload.password).map_err(|e| { + ( + StatusCode::INTERNAL_SERVER_ERROR, + format!("Password hashing failed: {}", e), + ) + })?; - let hashed_password = hash_password(&payload.password) - .map_err(|_| (StatusCode::INTERNAL_SERVER_ERROR, "Password hashing failed"))?; - - let conn = state.logs_pool.get() - .map_err(|_| (StatusCode::INTERNAL_SERVER_ERROR, "DB connection error"))?; + let conn = state.logs_pool.get().map_err(|e| { + ( + StatusCode::INTERNAL_SERVER_ERROR, + format!("DB connection error: {}", e), + ) + })?; conn.execute( - "INSERT INTO users (username, password, hotel_id, displayname) VALUES (?1, ?2, ?3, ?4)", - params![payload.username, hashed_password, payload.hotel_id, payload.displayname], + "INSERT INTO users (username, password, displayname) + VALUES (?1, ?2, ?3)", + params![payload.username, hashed_password, payload.displayname], ) - .map_err(|_| (StatusCode::INTERNAL_SERVER_ERROR, "DB insert error"))?; + .map_err(|e| { + ( + StatusCode::INTERNAL_SERVER_ERROR, + format!("User insert error: {}", e), + ) + })?; - Ok((StatusCode::CREATED, "User registered successfully")) + let user_id = conn.last_insert_rowid(); + + for &hotel_id in &payload.hotel_ids { + // more logic for security here + //FIXME: needs to be the display name in the DB, scheme is currently wrong + + let hotel_name: String = conn + .query_row( + "SELECT hotelname FROM hotels + WHERE id = ?1 ", + params![hotel_id], + |row| row.get(0), + ) + .map_err(|e| { + ( + StatusCode::BAD_REQUEST, + format!("Invalid hotel id {}: {}", hotel_id, e), + ) + })?; + + conn.execute( + "INSERT INTO hotel_user_link (user_id, hotel_id, username, hotelname) + VALUES (?1, ?2, ?3, ?4)", + params![user_id, hotel_id, payload.username, hotel_name], + ) + .map_err(|e| { + ( + StatusCode::INTERNAL_SERVER_ERROR, + format!( + "Link insert error for user_id={} hotel_id={}: {}", + user_id, hotel_id, e + ), + ) + })?; + } + + Ok(( + StatusCode::CREATED, + "User registered successfully".to_string(), + )) } #[derive(Serialize, Deserialize, Debug)] -pub struct ForceUpdatePasswordValues{ +pub struct ForceUpdatePasswordValues { username: String, newpassword: String, hotel_id: i32, @@ -175,46 +246,47 @@ pub struct ForceUpdatePasswordValues{ //pub struct ForceUpdatePasswordPayload (pub ForceUpdatePasswordValues); -pub async fn ForceUpdatePassword( +pub async fn force_update_password( State(state): State, Json(payload): Json, ) -> impl IntoResponse { - let conn = match state.logs_pool.get() { Ok(c) => c, - Err(_) => return (StatusCode::INTERNAL_SERVER_ERROR, "DB, conn failed").into_response() + Err(_) => return (StatusCode::INTERNAL_SERVER_ERROR, "DB, conn failed").into_response(), }; - let user_row = match conn.query_row( - "SELECT id FROM users WHERE username = ?1 AND hotel_id = ?2", - params![&payload.username, &payload.hotel_id], - |row|{ - let user_id: i32 = row.get(0)?; - //let hotel_id: i32 = row.get(1)?; - Ok((user_id)) - }, - ).optional() { + let user_row = match conn + .query_row( + "SELECT id FROM users WHERE username = ?1 AND hotel_id = ?2", + params![&payload.username, &payload.hotel_id], + |row| { + let user_id: i32 = row.get(0)?; + //let hotel_id: i32 = row.get(1)?; + Ok(user_id) + }, + ) + .optional() + { Ok(opt) => opt, - Err(_) => return (StatusCode::INTERNAL_SERVER_ERROR, "DB query error") - .into_response(), + Err(_) => return (StatusCode::INTERNAL_SERVER_ERROR, "DB query error").into_response(), }; - - let (user_id) = match user_row { + + let user_id = match user_row { Some(u) => u, - None => return (StatusCode::UNAUTHORIZED, "Not correct user") - .into_response(), + None => return (StatusCode::UNAUTHORIZED, "Not correct user").into_response(), }; - + let admin_check: String = "my_admin_password".to_string(); if &payload.admin_pass != &admin_check { - return (StatusCode::UNAUTHORIZED, "Invalid Amin Password").into_response() + return (StatusCode::UNAUTHORIZED, "Invalid Amin Password").into_response(); }; - let hashed_password = match hash_password(&payload.newpassword) { Ok(h) => h, - Err(_) => return (StatusCode::INTERNAL_SERVER_ERROR, "Password hashing failed").into_response(), + Err(_) => { + return (StatusCode::INTERNAL_SERVER_ERROR, "Password hashing failed").into_response(); + } }; let result = conn.execute( @@ -225,58 +297,67 @@ pub async fn ForceUpdatePassword( match result { Ok(rows) if rows > 0 => (StatusCode::OK, "Password updated").into_response(), Ok(_) => (StatusCode::NOT_FOUND, "User not found").into_response(), - Err(_) => (StatusCode::INTERNAL_SERVER_ERROR, "Failed to update password").into_response(), + Err(_) => ( + StatusCode::INTERNAL_SERVER_ERROR, + "Failed to update password", + ) + .into_response(), } - } #[derive(Serialize, Deserialize, Debug)] -pub struct UpdatePasswordValues{ +pub struct UpdatePasswordValues { username: String, current_password: String, newpassword: String, //hotel_id: i32, - } -pub async fn UpdatePassword( +pub async fn update_password( State(state): State, Json(payload): Json, ) -> impl IntoResponse { - let conn = match state.logs_pool.get() { Ok(c) => c, - Err(_) => return (StatusCode::INTERNAL_SERVER_ERROR, "DB, conn failed").into_response() + Err(_) => return (StatusCode::INTERNAL_SERVER_ERROR, "DB, conn failed").into_response(), }; - let user_row = match conn.query_row( - "SELECT password, id FROM users WHERE username = ?1 AND current_password = ?2", - params![&payload.username, &payload.current_password], - |row|{ - let password: String = row.get(0)?; - let id: i32 = row.get(1)?; - Ok((password, id)) - }, - ).optional() { + let user_row = match conn + .query_row( + "SELECT password, id FROM users WHERE username = ?1", + params![&payload.username], + |row| { + let password: String = row.get(0)?; + let id: i32 = row.get(1)?; + Ok((password, id)) + }, + ) + .optional() + { Ok(opt) => opt, - Err(_) => return (StatusCode::INTERNAL_SERVER_ERROR, "DB query error") - .into_response(), + Err(e) => { + return ( + StatusCode::INTERNAL_SERVER_ERROR, + format!("DB query error: {}", e), + ) + .into_response(); + } }; - + let (password, user_id) = match user_row { Some(u) => u, - None => return (StatusCode::UNAUTHORIZED, "Not correct user") - .into_response(), - }; - - if verify_password( &payload.current_password, &password ) { - return (StatusCode::UNAUTHORIZED, "Invalid Password").into_response() + None => return (StatusCode::UNAUTHORIZED, "Not correct user").into_response(), + }; + + if !verify_password(&payload.current_password, &password) { + return (StatusCode::UNAUTHORIZED, "Invalid Password").into_response(); }; - let hashed_password = match hash_password(&payload.newpassword) { Ok(h) => h, - Err(_) => return (StatusCode::INTERNAL_SERVER_ERROR, "Password hashing failed").into_response(), + Err(_) => { + return (StatusCode::INTERNAL_SERVER_ERROR, "Password hashing failed").into_response(); + } }; let result = conn.execute( @@ -287,22 +368,26 @@ pub async fn UpdatePassword( match result { Ok(rows) if rows > 0 => (StatusCode::OK, "Password updated").into_response(), Ok(_) => (StatusCode::NOT_FOUND, "User not found").into_response(), - Err(_) => (StatusCode::INTERNAL_SERVER_ERROR, "Failed to update password").into_response(), + Err(_) => ( + StatusCode::INTERNAL_SERVER_ERROR, + "Failed to update password", + ) + .into_response(), } - } #[derive(Deserialize, Debug)] pub struct LoginValues { - username : String, - password : String, + username: String, + password: String, //hotel_id: i32, } pub struct LoginPayload(pub LoginValues); impl FromRequest for LoginPayload -where S: Send + Sync, +where + S: Send + Sync, { type Rejection = (StatusCode, String); @@ -310,13 +395,13 @@ where S: Send + Sync, let Json(payload) = Json::::from_request(req, state) .await .map_err(|err| (StatusCode::BAD_REQUEST, format!("Invalid body: {}", err)))?; - + Ok(LoginPayload(payload)) } } -#[derive(Deserialize,Debug, Serialize, Clone)] -struct Claims{ +#[derive(Deserialize, Debug, Serialize, Clone)] +struct Claims { id: i32, hotel_id: i32, //display_name @@ -331,6 +416,7 @@ struct LoginResponse { #[derive(Serialize)] struct MultiLoginResponse { + user_id: i32, tokens: Vec, } @@ -342,41 +428,49 @@ pub async fn clean_auth_loging( // 1️⃣ Get a connection from logs pool let conn = match state.logs_pool.get() { Ok(c) => c, - Err(_) => return (StatusCode::INTERNAL_SERVER_ERROR, "DB connection error").into_response(), + Err(_) => { + return (StatusCode::INTERNAL_SERVER_ERROR, "DB connection error").into_response(); + } }; - - let user_row = match conn.query_row( - "SELECT id, password, hotel_id, displayname FROM users WHERE username = ?1", - params![&payload.username], - |row| { - let user_id: i32 = row.get(0)?; - let password: String = row.get(1)?; - let hotel_id: i32 = row.get(2)?; - let displayname: String = row.get(3)?; - Ok((user_id, password, hotel_id, displayname)) - }, - ).optional() { + + let user_row = match conn + .query_row( + "SELECT id, password, hotel_id, displayname FROM users WHERE username = ?1", + params![&payload.username], + |row| { + let user_id: i32 = row.get(0)?; + let password: String = row.get(1)?; + let hotel_id: i32 = row.get(2)?; + let displayname: String = row.get(3)?; + Ok((user_id, password, hotel_id, displayname)) + }, + ) + .optional() + { Ok(opt) => opt, Err(_) => return (StatusCode::INTERNAL_SERVER_ERROR, "DB query error").into_response(), }; - let (user_id, stored_hash, hotel_id, displayname) = match user_row { - Some(u) => u, - None => return (StatusCode::UNAUTHORIZED, "Invalid credentials").into_response(), + let (user_id, stored_hash, hotel_id, _displayname) = match user_row { + Some(u) => u, + None => return (StatusCode::UNAUTHORIZED, "Invalid credentials").into_response(), }; if !verify_password(&payload.password, &stored_hash) { - return (StatusCode::UNAUTHORIZED, "Invelid credentials").into_response(); + return (StatusCode::UNAUTHORIZED, "Invalid credentials").into_response(); } - - let expiration = match chrono::Utc::now().checked_add_signed(chrono::Duration::hours(15)) { - Some(time) => time.timestamp() as usize, - None => { - // Handle overflow — probably a 500, since this should never happen - return (StatusCode::INTERNAL_SERVER_ERROR, "Time overflow".to_string()).into_response(); - } -}; + let expiration = match chrono::Utc::now().checked_add_signed(chrono::Duration::hours(15)) { + Some(time) => time.timestamp() as usize, + None => { + // Handle overflow — probably a 500, since this should never happen + return ( + StatusCode::INTERNAL_SERVER_ERROR, + "Time overflow".to_string(), + ) + .into_response(); + } + }; let claims = serde_json::json!({ "id": user_id, @@ -385,13 +479,11 @@ pub async fn clean_auth_loging( "exp": expiration }); - let token = match encode( - &Header::default(), - &claims, - &keys.encoding - ) { + let token = match encode(&Header::default(), &claims, &keys.encoding) { Ok(t) => t, - Err(_) => return (StatusCode::INTERNAL_SERVER_ERROR, "JWT creation failed").into_response(), + Err(_) => { + return (StatusCode::INTERNAL_SERVER_ERROR, "JWT creation failed").into_response(); + } }; Json(LoginResponse { token }).into_response() } @@ -402,16 +494,17 @@ pub struct CreateRefreshTokenValue { pub password: String, pub device_id: Uuid, //pub timestamp: Option, - } -//TODO: refactor this to impl IntoResponse ans not Result +//FIXME: weird return type, returning result ? + #[axum::debug_handler] pub async fn create_refresh_token( State(state): State, user_agent: Option>, - Json(payload): Json -) -> Result { // ← Add Result here + Json(payload): Json, +) -> Result { + // ← Add Result here let user_agent_str = user_agent .map(|ua| ua.to_string()) @@ -419,279 +512,669 @@ pub async fn create_refresh_token( let device_id_str = payload.device_id.to_string(); + let conn = state.logs_pool.get().map_err(|_| { + ( + StatusCode::INTERNAL_SERVER_ERROR, + "DB connection error".to_string(), + ) + })?; + let argon2 = Argon2::default(); let salt = SaltString::generate(&mut OsRng); let mut bytes = [0u8; 64]; - OsRng.fill_bytes(&mut bytes); + OsRng.fill_bytes(&mut bytes); let raw_token = Uuid::new_v4().to_string(); + let hashed_token = &raw_token; + /* let hashed_token = argon2 .hash_password(raw_token.as_bytes(), &salt) .map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()))? .to_string(); + */ - let conn = state.logs_pool.get() - .map_err(|_| (StatusCode::INTERNAL_SERVER_ERROR, "DB connection error".to_string()))?; + // let mut stmt = conn.prepare( + // "SELECT id, password FROM users WHERE username = ?1" - let mut stmt = conn.prepare( - "SELECT id, password, hotel_id FROM users WHERE username = ?1" - ).map_err(|_| (StatusCode::INTERNAL_SERVER_ERROR, "DB prepare error".to_string()))?; - - - let user_rows = stmt - .query_map(params![&payload.username], |row| { + let credentials = match conn.query_row( + "SELECT id, password FROM users WHERE username = ?1", + params![&payload.username], + |row| { let user_id: i32 = row.get(0)?; let password: String = row.get(1)?; - let hotel_id: i32 = row.get(2)?; - Ok((user_id, password, hotel_id)) - }) - .map_err(|_| (StatusCode::INTERNAL_SERVER_ERROR, "DB query error".to_string()))?; - -/* - let (user_id, stored_hash, hotel_id) = user_row - .ok_or((StatusCode::NOT_FOUND, "User not found".to_string()))?; -*/ - //let mut tokens = Vec::new(); - - for user_row_result in user_rows { - let (user_id, stored_hash, hotel_id) = user_row_result - .map_err(|_| (StatusCode::INTERNAL_SERVER_ERROR, "DB row error".to_string()))?; - - if !verify_password(&payload.password, &stored_hash) { - continue; // Skip rows with invalid password + Ok((user_id, password)) + }, + ) { + Ok(cr) => cr, + Err(e) => { + return Err(( + StatusCode::INTERNAL_SERVER_ERROR, + format!("error fetching credentials: {e}").to_string(), + )); } + }; - /* - let mut bytes = [0u8; 64]; - OsRng.fill_bytes(&mut bytes); - let raw_token = Uuid::new_v4().to_string(); + let (user_id, user_password) = credentials; - let hashed_token = argon2 - .hash_password(raw_token.as_bytes(), &salt) - .map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()))? - .to_string(); - */ - - conn.execute( - "INSERT INTO refresh_token (user_id, token_hash, device_id, user_agent, hotel_id) VALUES (?1, ?2, ?3, ?4, ?5)", - params![user_id, hashed_token, device_id_str, user_agent_str, hotel_id], - ) - .map_err(|_| (StatusCode::INTERNAL_SERVER_ERROR, "DB insert error".to_string()))?; - - //tokens.push(raw_token); + /* + let (user_id, stored_hash, hotel_id) = user_row + .ok_or((StatusCode::NOT_FOUND, "User not found".to_string()))?; + */ + //let mut tokens = Vec::new(); + //TODO: validate password + if !verify_password(&payload.password, &user_password) { + return Err(( + StatusCode::INTERNAL_SERVER_ERROR, + "Invalid credential".to_string(), + )); // Skip rows with invalid password } + //TODO: get hotel name to return a map/tuple of hotel name + let mut stmt = match conn.prepare("SELECt hotel_id FROM hotel_user_link WHERE user_id = ?1") { + Ok(stmt) => stmt, + Err(_) => { + return Err(( + StatusCode::INTERNAL_SERVER_ERROR, + "error building user_id fetch stmt".to_string(), + )); + } + }; - let cookie_value = format!("refresh_token={}; HttpOnly; Secure; Path=/", raw_token); + //TODO: compiler les hotel id dans un vecteur pour le feed dans le refresh token + let hotel_ids: Vec = match stmt.query_map(params![&user_id], |row| row.get(0)) { + Ok(rows) => rows.collect::, _>>().map_err(|_| { + ( + StatusCode::INTERNAL_SERVER_ERROR, + "Error collecting hotel_ids".to_string(), + ) + })?, + Err(_) => { + return Err(( + StatusCode::INTERNAL_SERVER_ERROR, + "Error mapping hotel_ids".to_string(), + )); + } + }; - let mut response = (StatusCode::CREATED, "Refresh token created successfully").into_response(); - response.headers_mut().insert( - SET_COOKIE, - HeaderValue::from_str(&cookie_value).unwrap(), + let hotel_ids_json = match serde_json::to_string(&hotel_ids) { + Ok(json) => json, + Err(_) => { + return Err(( + StatusCode::INTERNAL_SERVER_ERROR, + "Error mapping hotel_ids".to_string(), + )); + } + }; + + /*.map_err(|_| (StatusCode::INTERNAL_SERVER_ERROR, "Error mapping hotel_ids".to_string())); */ + + //FIXME: might not need the hotel list on tconflict ? + + //TODO: remove user agent entirely from auth ,it is mutable and not stable + //TODO: make the token refresh on login + conn.execute( + r#" + INSERT INTO refresh_token ( + user_id, + token_hash, + device_id, + hotel_id_list + ) + VALUES (?1, ?2, ?3, ?4) + ON CONFLICT(user_id, device_id) + DO UPDATE SET + token_hash = excluded.token_hash, + hotel_id_list = excluded.hotel_id_list + "#, + params![user_id, hashed_token, device_id_str, hotel_ids_json], + ) + .map_err(|e| { + ( + StatusCode::INTERNAL_SERVER_ERROR, + format!("DB error: {}", e), + ) + })?; + + //TODO: add a map/tupple of of the allowed hotels and their id+name, maybe update the token ? + + println!("RAW write refresh_token bytes: {:?}", &raw_token.as_bytes()); + println!("RAW refresh_token : {}", &raw_token.to_string()); + println!("RAW write refresh_token len: {}", &raw_token.len()); + + let cookie_value = format!( + "refresh_token={}; HttpOnly; Secure; Max-Age=60480000000;Path=/", + raw_token ); - Ok(response) // ← Wrap in Ok() + let mut response = (StatusCode::CREATED, "Refresh token created successfully").into_response(); + response + .headers_mut() + .insert(SET_COOKIE, HeaderValue::from_str(&cookie_value).unwrap()); + + Ok(response) // ← Wrap in Ok() } #[derive(Deserialize)] -pub struct LoginRefreshTokenValues{ +pub struct LoginRefreshTokenValues { device_id: Uuid, - refresh_token: String, + //refresh_token: String, } -pub async fn login_refresh_token ( +//TODO: LATER : implement hotel-id-selected to allow user to only get part hotels ? +pub async fn login_refresh_token( State(state): State, Extension(keys): Extension, user_agent: Option>, - Json(payload): Json + cookie_header: Option>, + Json(payload): Json, ) -> impl IntoResponse { + println!("login_refresh_token called"); + // Log cookies + + let cookies = match cookie_header { + Some(token) => token, + None => return (StatusCode::UNAUTHORIZED, "Missing refresh token cookie").into_response(), + }; + + let refresh_token = match cookies.get("refresh_token") { + Some(token) => token.to_string(), + None => return (StatusCode::UNAUTHORIZED, "Missing refresh token cookie").into_response(), + }; + + println!("RAW refresh_token bytes: {:?}", refresh_token.as_bytes()); + println!("RAW refresh_token : {}", refresh_token.to_string()); + println!("RAW refresh_token len: {}", refresh_token.len()); + + println!("Cookies: {:?}", &refresh_token); let conn = match state.logs_pool.get() { Ok(c) => c, - Err(_) => return (StatusCode::INTERNAL_SERVER_ERROR, "DB connection error").into_response(), + Err(_) => { + return (StatusCode::INTERNAL_SERVER_ERROR, "DB connection error").into_response(); + } }; - - /*let user_agent_str = user_agent - .map(|ua| ua.to_string()) - .unwrap_or_else(|| "Unknown".to_string()); - + let user_agent_str = match user_agent { + Some(ua) => ua.to_string(), + None => return (StatusCode::INTERNAL_SERVER_ERROR, "user agent unknown").into_response(), + }; + println!("UA {:?}", &user_agent_str); let device_id_str = payload.device_id.to_string(); - */ -let user_agent_str = match user_agent { - Some(ua) => ua.to_string(), - None => return (StatusCode::INTERNAL_SERVER_ERROR, "user agent unknown").into_response(), -}; - -let device_id_str = payload.device_id.to_string(); + println!("device id: {:?}", &device_id_str); - //"SELECT user_id, token_hash, hotel_id FROM refresh_token WHERE device_id = ?1 AND user_agent = ?2", + //TODO: swap to query row and get hotel-id's list and not single hotel per row + //deserializing the list : + //let hotel_ids: Vec = serde_json::from_str(&stored_value)?; let mut stmt = match conn.prepare( - "SELECT user_id, token_hash, hotel_id + "SELECT user_id, hotel_id_list FROM refresh_token - WHERE device_id = ?1 AND user_agent = ?2" + WHERE device_id = ?1 AND token_hash = ?2 + LIMIT 1;", ) { Ok(s) => s, - Err(_) => return (StatusCode::INTERNAL_SERVER_ERROR, "DB query error").into_response(), - }; - - let rows = match stmt.query_map(params![&device_id_str, &user_agent_str], |row| { - Ok(( - row.get::<_, i32>(0)?, // user_id - row.get::<_, String>(1)?, // token_hash - row.get::<_, i32>(2)?, // hotel_id - )) - }) { - Ok(r) => r, - Err(_) => return (StatusCode::INTERNAL_SERVER_ERROR, "DB query error").into_response(), - }; - - let mut entries = Vec::new(); - for r in rows { - match r { - Ok(t) => entries.push(t), - Err(_) => continue, // ignore corrupt rows + Err(_) => { + return ( + StatusCode::INTERNAL_SERVER_ERROR, + "Error prepatring hotel_id_list stmt", + ) + .into_response(); } - } + }; - if entries.is_empty() { + let rows = match stmt + .query_one(params![&device_id_str, &refresh_token], |row| { + Ok(( + row.get::<_, i32>(0)?, // user_id + row.get::<_, String>(1)?, // token_hash + //row.get::<_, String>(2)?, // hotel_id //FIXME: this is supposed to be vectore maybe ? + )) + }) + .optional() + { + Ok(r) => r, + Err(e) => { + eprintln!("DB ERROR: {:?}", e); + return ( + StatusCode::INTERNAL_SERVER_ERROR, + format!("DB query error: {}", e), + ) + .into_response(); + } + }; + //TODO: extraction of the blob + //let json_hotel_ids = rows.2; + let (user_id, json_hotel_ids) = match rows { + Some(r) => r, + None => { + return ( + StatusCode::UNAUTHORIZED, + "No refresh token found for this device", + ) + .into_response(); + } + }; + let hotel_ids: Vec = match serde_json::from_str(&json_hotel_ids) { + Ok(ids) => ids, + Err(_) => { + return ( + StatusCode::INTERNAL_SERVER_ERROR, + "Hotel ids are not deserializable to Vec", + ) + .into_response(); + } + }; + + //FIXME: still problems when corrupted token exist + if hotel_ids.is_empty() { return (StatusCode::UNAUTHORIZED, "No matching device").into_response(); } + /* + + eprintln!("DB ERROR: {:?}", &refresh_token); + eprintln!("DB ERROR: {:?}", &token); + + + //still not auto adding hotel user link when creating account + if (&refresh_token != &token) { + // skip rows with wrong hash + return (StatusCode::UNAUTHORIZED, "Invelid credentials").into_response(); + } + */ + let expiration = match chrono::Utc::now().checked_add_signed(chrono::Duration::hours(15)) { + Some(time) => time.timestamp() as usize, + None => { + // Handle overflow — probably a 500, since this should never happen + return ( + StatusCode::INTERNAL_SERVER_ERROR, + "Time overflow".to_string(), + ) + .into_response(); + } + }; + let mut tokens = Vec::new(); -for (user_id, token_hash, hotel_id) in entries { - if !verify_password(&payload.refresh_token, &token_hash) { - // skip rows with wrong hash - continue; + for hotel_id in hotel_ids { + let claims = serde_json::json!({ + "id": user_id, + "hotel_id": hotel_id, + "exp": expiration + }); + + let token = match encode(&Header::default(), &claims, &keys.encoding) { + Ok(token) => token, + Err(_) => { + return (StatusCode::INTERNAL_SERVER_ERROR, "JWT creation failed").into_response(); + } + }; + + tokens.push(token); } - let expiration = match chrono::Utc::now().checked_add_signed(chrono::Duration::hours(15)) { - Some(time) => time.timestamp() as usize, - None => { - // Handle overflow — probably a 500, since this should never happen - return (StatusCode::INTERNAL_SERVER_ERROR, "Time overflow".to_string()).into_response(); + if tokens.is_empty() { + return (StatusCode::UNAUTHORIZED, "Invalid or mismatched token").into_response(); } - }; - let claims = serde_json::json!({ - "id": user_id, - "hotel_id": hotel_id, - "exp": expiration - }); - - let token = match encode(&Header::default(), &claims, &keys.encoding) { - Ok(t) => t, - Err(_) => return (StatusCode::INTERNAL_SERVER_ERROR, "JWT creation failed").into_response(), - }; - - tokens.push(token); + //Json(tokens).into_response() + Json(MultiLoginResponse { user_id, tokens }).into_response() } - -if tokens.is_empty() { - return (StatusCode::UNAUTHORIZED, "Invalid or mismatched token").into_response(); -} - -//Json(tokens).into_response() - Json(MultiLoginResponse { tokens }).into_response() -} - -pub async fn logout_from_single_device ( +#[axum::debug_handler] +pub async fn logout_from_single_device( State(state): State, Extension(keys): Extension, user_agent: Option>, - Json(payload): Json + cookie_header: Option>, + Json(payload): Json, ) -> impl IntoResponse { - let user_agent_str = user_agent - .map(|ua| ua.to_string()) + .map(|TypedHeader(ua)| ua.as_str().to_owned()) .unwrap_or_else(|| "Unknown".to_string()); - + + let cookies = match cookie_header { + Some(token) => token, + None => return (StatusCode::UNAUTHORIZED, "Missing refresh token cookie").into_response(), + }; + + let refresh_token = match cookies.get("refresh_token") { + Some(token) => token.to_string(), + None => return (StatusCode::UNAUTHORIZED, "Missing refresh token cookie").into_response(), + }; + let device_id_str = payload.device_id.to_string(); let conn = match state.logs_pool.get() { Ok(c) => c, - Err(_) => return (StatusCode::INTERNAL_SERVER_ERROR, "DB connection error").into_response(), - }; - - let device_row = match conn.query_row( - "SELECT user_id, token_hash, hotel_id, id FROM refresh_token WHERE device_id = ?1 AND user_agent = ?2 AND revoke = 0 ", - params![&device_id_str, &user_agent_str], - |row| { - let user_id: i32 = row.get(0)?; - let token_hash: String = row.get(1)?; - let hotel_id: i32 = row.get(2)?; - let id:i32 = row.get(3)?; - //let displayname: String = row.get(3)?; - Ok((user_id, token_hash, hotel_id,id)) - }, - ).optional() { - Ok(opt) => opt, - Err(_) => return (StatusCode::INTERNAL_SERVER_ERROR, "DB query error").into_response(), + Err(_) => { + return (StatusCode::INTERNAL_SERVER_ERROR, "DB connection error").into_response(); + } }; - let (user_id, token_hash, hotel_id, token_id) = match device_row { + let device_row = match conn + .query_row( + "SELECT user_id, hotel_id_list, id + FROM refresh_token + WHERE token_hash = ?1 AND revoked = 0 ", + params![&refresh_token], + |row| { + let user_id: i32 = row.get(0)?; + let json_hotel_id_list: String = row.get(1)?; + let id: i32 = row.get(2)?; + //let displayname: String = row.get(3)?; + Ok((user_id, json_hotel_id_list, id)) + }, + ) + .optional() + { + Ok(opt) => opt, + Err(e) => { + return ( + StatusCode::INTERNAL_SERVER_ERROR, + format!("DB query error : {}", e), + ) + .into_response(); + } + }; + + let (user_id, json_hotel_id_list, token_id) = match device_row { Some(tuple) => tuple, None => return (StatusCode::UNAUTHORIZED, "No matching device").into_response(), }; - if !verify_password(&payload.refresh_token, &token_hash) { - return (StatusCode::UNAUTHORIZED, "Invalid or mismatched token").into_response(); - } + let hotel_ids: Vec = match serde_json::from_str(&json_hotel_id_list) { + Ok(ids) => ids, + Err(_) => { + return ( + StatusCode::INTERNAL_SERVER_ERROR, + "Hotel ids are not deserializable to Vec", + ) + .into_response(); + } + }; + + //FIXME: need to chang the way we get refresh token from the cookies instead + /* + if !verify_password(&payload.refresh_token, &token_hash) { + return (StatusCode::UNAUTHORIZED, "Invalid or mismatched token").into_response(); + } + */ let revoked: Result = conn.query_row( - "UPDATE refresh_token SET revoked = 1 WHERE id = ?1 RETURNING device_id", + "DELETE FROM refresh_token + WHERE id = ?1 + RETURNING device_id", params![&token_id], |row| row.get(0), ); - return (StatusCode::OK, format!("Token deleted for device id {}", &device_id_str)).into_response() + let revoked_id = match (revoked) { + Ok(r) => r, + Err(e) => { + return ( + StatusCode::INTERNAL_SERVER_ERROR, + "Hotel ids are not deserializable to Vec", + ) + .into_response(); + } + }; + let cookie_value = format!( + "refresh_token={}; HttpOnly; Secure; Max-Age=0;Path=/", + "loggedout" + ); + + let mut response = ( + StatusCode::CREATED, + format!("Token deleted for device id {}", &revoked_id), + ) + .into_response(); + + response + .headers_mut() + .insert(SET_COOKIE, HeaderValue::from_str(&cookie_value).unwrap()); + + response } -pub async fn logout_from_all_devices ( +pub async fn logout_from_all_devices( State(state): State, Extension(keys): Extension, AuthClaims { user_id, hotel_id }: AuthClaims, - Json(payload): Json + //Json(payload): Json ) -> impl IntoResponse { - - - - let device_id_str = payload.device_id.to_string(); + //let device_id_str = payload.device_id.to_string(); let conn = match state.logs_pool.get() { Ok(c) => c, - Err(_) => return (StatusCode::INTERNAL_SERVER_ERROR, "DB connection error").into_response(), + Err(_) => { + return (StatusCode::INTERNAL_SERVER_ERROR, "DB connection error").into_response(); + } }; - + let result = conn.execute( - "UPDATE refresh_token SET revoked = 1 WHERE user_id = ?1 AND revoked = 0", + "DELETE FROM refresh_token WHERE user_id = ?1", params![&user_id], ); + /* + + match result { + //Ok(count) if count > 0 => { + // (StatusCode::OK, format!("Revoked {} active tokens", count)).into_response() + //} + //Ok(_) => (StatusCode::NOT_FOUND, "No active tokens to revoke").into_response(), + Err(_) => ( + StatusCode::INTERNAL_SERVER_ERROR, + "Database update error".to_string(), + ) + .into_response(), + } + */ + + let cookie_value = format!( + "refresh_token={}; HttpOnly; Secure; Max-Age=0;Path=/", + "loggedout" + ); + + let mut response = + (StatusCode::CREATED, format!("Token deleted for device id ")).into_response(); + + response + .headers_mut() + .insert(SET_COOKIE, HeaderValue::from_str(&cookie_value).unwrap()); + match result { - Ok(count) if count > 0 => { - (StatusCode::OK, format!("Revoked {} active tokens", count)).into_response() - } - Ok(_) => (StatusCode::NOT_FOUND, "No active tokens to revoke").into_response(), - Err(_) => ( + //Ok(count) if count > 0 => { + // (StatusCode::OK, format!("Revoked {} active tokens", count)).into_response() + //} + Ok(_) => response, + Err(err) => ( StatusCode::INTERNAL_SERVER_ERROR, - "Database update error".to_string(), + err.to_string(), // or format!("{err:?}") ) .into_response(), } - - + //response } +#[derive(Serialize)] +struct HotelData { + id: i32, + hotel_name: String, +} +pub async fn get_hotel(State(state): State) -> impl IntoResponse { + let try_conn = state.logs_pool.get(); + + let conn = match try_conn { + Ok(conn) => conn, + Err(e) => return (StatusCode::INTERNAL_SERVER_ERROR, "bruh").into_response(), + }; + + let try_stmt = conn.prepare( + " + SELECT id, hotelname + FROM hotels", + ); + + let mut stmt = match try_stmt { + Ok(stmt) => stmt, + Err(e) => { + return ( + StatusCode::INTERNAL_SERVER_ERROR, + "failed buildin statement", + ) + .into_response(); + } + }; + + let try_hotels = stmt.query_map(params![], |row| { + Ok(HotelData { + id: row.get(0)?, + hotel_name: row.get(1)?, + }) + }); + + let hotel_itter = match try_hotels { + Ok(hotels) => hotels, + Err(e) => { + return ( + StatusCode::INTERNAL_SERVER_ERROR, + "error processing hotel list", + ) + .into_response(); + } + }; + + let hotels: Vec = match hotel_itter.collect::, _>>() { + Ok(hotel) => hotel, + Err(e) => { + return ( + StatusCode::INTERNAL_SERVER_ERROR, + format!("failed collection of hotel : {e}"), + ) + .into_response(); + } + }; + + match serde_json::to_string(&hotels) { + Ok(json) => return (StatusCode::OK, json).into_response(), + Err(e) => { + return ( + StatusCode::INTERNAL_SERVER_ERROR, + format!("Serialization failed: {}", e), + ) + .into_response(); + } + }; + + //.map_err(|_| (StatusCode::INTERNAL_SERVER_ERROR, "DB connection error".to_string()))?; + //return (StatusCode::OK).into_response(); +} + +#[derive(Deserialize, Debug)] +pub struct addHotelUser { + user_id: i32, + #[serde(default)] + hotel_ids: Vec, +} + +pub async fn add_hotel_user( + State(state): State, + Extension(keys): Extension, + Json(payload): Json, +) -> impl IntoResponse { + let conn = match state.logs_pool.get() { + Ok(c) => c, + Err(e) => { + return (StatusCode::INTERNAL_SERVER_ERROR, "DB connection error").into_response(); + } + }; + + let user_name: String = match conn.query_row( + "SELECT username FROM users WHERE id = ?1", + params![&payload.user_id], + |row| row.get(0), + ) { + Ok(name) => name, + Err(e) => { + return ( + StatusCode::INTERNAL_SERVER_ERROR, + format!("user not found {e} "), + ) + .into_response(); + } + }; + + let mut get_hotel_name_stmt = match conn.prepare("SELECT hotelname FROM hotels WHERE id = ?1") { + Ok(stmt) => stmt, + Err(e) => { + return ( + StatusCode::INTERNAL_SERVER_ERROR, + format!("could't prepare stmt for hotel : {e} "), + ) + .into_response(); + } + }; + + let mut insert_hotel_link_stmt = match conn.prepare( + "INSERT INTO hotel_user_link + (user_id,hotel_id,username,hotelname) + VALUES (?1,?2,?3,?4)", + ) { + Ok(stmt) => stmt, + Err(e) => { + return ( + StatusCode::INTERNAL_SERVER_ERROR, + format!("could't prepare stmt to insert hotel : {e} "), + ) + .into_response(); + } + }; + + for &hotel_id in &payload.hotel_ids { + let hotel_name: String = + match get_hotel_name_stmt.query_row(params![hotel_id], |row| row.get(0)) { + Ok(name) => name, + Err(e) => { + return ( + StatusCode::INTERNAL_SERVER_ERROR, + format!("hotel not found {e} "), + ) + .into_response(); + } + }; + + let add_link = match conn.execute( + "INSERT INTO hotel_user_link + (user_id,hotel_id,username,hotelname) + VALUES (?1,?2,?3,?4)", + params![payload.user_id, hotel_id, user_name, hotel_name], + ) { + Ok(_) => true, + Err(e) => { + return ( + StatusCode::INTERNAL_SERVER_ERROR, + format!("hotel not found {e} "), + ) + .into_response(); + } + }; + + //TODO: still need to build the add hotel to user here + } + + return (StatusCode::OK, "goo").into_response(); +} fn internal_error(err: E) -> (StatusCode, String) { - (StatusCode::INTERNAL_SERVER_ERROR, format!("Internal error: {}", err)) -} \ No newline at end of file + ( + StatusCode::INTERNAL_SERVER_ERROR, + format!("Internal error: {}", err), + ) +} diff --git a/src/utils/db_pool.rs b/src/utils/db_pool.rs index 6be8c1f..150b5ef 100644 --- a/src/utils/db_pool.rs +++ b/src/utils/db_pool.rs @@ -1,19 +1,21 @@ -use std::sync::Arc; +use axum::extract::{ + State, + ws::{Message, WebSocket, WebSocketUpgrade}, +}; use dashmap::DashMap; -use r2d2::{Pool}; +use r2d2::Pool; use r2d2_sqlite::SqliteConnectionManager; +use std::sync::Arc; use tokio::sync::mpsc; -use axum::extract::{ws::{Message, WebSocket, WebSocketUpgrade}, State}; use crate::utils::websocket::WsMap; - #[derive(Clone)] - pub struct AppState { - pub hotel_pools: HotelPool, - pub logs_pool: Pool, - pub ws_map: WsMap, - } - +#[derive(Clone)] +pub struct AppState { + pub hotel_pools: HotelPool, + pub logs_pool: Pool, + pub ws_map: WsMap, +} type HotelId = i32; // or i32 if you want numeric ids @@ -44,4 +46,4 @@ impl HotelPool { self.hotel_pools.insert(hotel_id, db_pool.clone()); db_pool } -} \ No newline at end of file +} diff --git a/src/utils/mod.rs b/src/utils/mod.rs index 3ec42dc..59d0bfa 100644 --- a/src/utils/mod.rs +++ b/src/utils/mod.rs @@ -1,4 +1,4 @@ -pub mod db_pool; pub mod auth; +pub mod db_pool; pub mod routes; -pub mod websocket; \ No newline at end of file +pub mod websocket; diff --git a/src/utils/routes.rs b/src/utils/routes.rs index 99ad04a..9314b0d 100644 --- a/src/utils/routes.rs +++ b/src/utils/routes.rs @@ -1,33 +1,29 @@ use axum::{ - routing::{get, put, post}, Router, + routing::{get, post, put}, }; use crate::utils::auth::*; -use crate::utils::db_pool::{HotelPool, AppState, }; +use crate::utils::db_pool::{AppState, HotelPool}; use crate::utils::websocket::ws_handler; -use headers::UserAgent; use axum_extra::TypedHeader; - +use headers::UserAgent; // ROOTS pub fn utils_routes() -> Router { - Router::new() .route("/login", put(clean_auth_loging)) .route("/register", put(register_user)) - .route("/ws/", get(ws_handler)) .route("/tokentest", put(token_tester)) - .route("/force_update_password", put(ForceUpdatePassword)) - .route("/update_password", put(UpdatePassword)) - + .route("/update_password", put(update_password)) .route("/create_refresh", post(create_refresh_token)) .route("/login_refresh_token", post(login_refresh_token)) - .route("/logout_single_device", post(logout_from_single_device)) .route("/logout_all_devices", post(logout_from_all_devices)) + .route("/ws/{req_token}", get(ws_handler)) + .route("/force_update_password", put(force_update_password)) + .route("/get_hotels", get(get_hotel)) + .route("/add_hotel_user", put(add_hotel_user)) - - - //.with_state(state) - } \ No newline at end of file + //.with_state(state) +} diff --git a/src/utils/websocket.rs b/src/utils/websocket.rs index 8c743d6..89ba323 100644 --- a/src/utils/websocket.rs +++ b/src/utils/websocket.rs @@ -1,31 +1,33 @@ -use dashmap::DashMap; -use std::sync::Arc; -use axum::extract::{ws::{Message, WebSocket, WebSocketUpgrade}, State}; -use tokio::sync::mpsc; use axum::extract::Path; use axum::response::IntoResponse; +use axum::{ + Extension, + extract::{ + State, + ws::{Message, WebSocket, WebSocketUpgrade}, + }, +}; +use dashmap::DashMap; +use reqwest::StatusCode; +use std::sync::Arc; +use tokio::sync::mpsc; //use futures_util::stream::stream::StreamExt; -use futures_util::{StreamExt, SinkExt}; +use futures_util::{SinkExt, StreamExt}; -use crate::utils::{auth::AuthClaims, db_pool::{AppState, HotelPool}}; +use crate::utils::{ + auth::{AuthClaims, JwtKeys, auth_claims_from_token}, + db_pool::{AppState, HotelPool}, +}; - - - /// Type alias: user_id → sender to that user +/// Type alias: user_id → sender to that user pub type UserMap = DashMap>; /// hotel_id → users pub type HotelMap = DashMap>; /// global map of all hotels pub type WsMap = Arc; - /// Type alias: user_id → sender to that user +/// Type alias: user_id → sender to that user - -async fn handle_socket( - mut socket: WebSocket, - state: AppState, - hotel_id: i32, - user_id: i32, -) { +async fn handle_socket(mut socket: WebSocket, state: AppState, hotel_id: i32, user_id: i32) { // channel for sending messages TO this client let (tx, mut rx) = mpsc::unbounded_channel::(); @@ -44,8 +46,6 @@ async fn handle_socket( // split socket into sender/receiver let (mut sender, mut receiver) = socket.split(); - - // task for sending messages from server to client let mut rx_task = tokio::spawn(async move { while let Some(msg) = rx.recv().await { @@ -89,12 +89,32 @@ async fn handle_socket( } pub async fn ws_handler( - AuthClaims {user_id, hotel_id}: AuthClaims, + //AuthClaims {user_id, hotel_id}: AuthClaims, ws: WebSocketUpgrade, + Extension(keys): Extension, State(state): State, - //Path((hotel_id, user_id)): Path<(i32, i32)>, + Path((req_token)): Path<(String)>, ) -> impl IntoResponse { - ws.on_upgrade(move |socket| handle_socket(socket, state, hotel_id, user_id)) + let token = req_token; + + let claims = match auth_claims_from_token(&token, &keys) { + Err(_) => { + print!("error during auth claims processing"); + return StatusCode::UNAUTHORIZED.into_response(); + } + Ok(c) => c, + }; + + print!("{token}, web socket tried to connect",); + + /* + let claims = match auth_claims_from_token(&token, &keys) { + Ok(c) => c, + Err(_) => return StatusCode::UNAUTHORIZED.into_response(), + }; + */ + + ws.on_upgrade(move |socket| handle_socket(socket, state, claims.hotel_id, claims.user_id)) } fn print_ws_state(state: &AppState) { @@ -106,4 +126,4 @@ fn print_ws_state(state: &AppState) { println!("Hotel {hotel_id}: users {:?}", users); } println!("--------------------------------"); -} \ No newline at end of file +} diff --git a/utils command.txt b/utils command.txt deleted file mode 100644 index 6600c89..0000000 --- a/utils command.txt +++ /dev/null @@ -1,99 +0,0 @@ -cross build --release --target aarch64-unknown-linux-gnu - -docker run -p 8080:8080 \ - -v ${PWD}/db:/db \ - -e JWT_SECRET="my-dev-secret" \ - rust-api:1.0.0 - -GOOD - - docker run - --hostname=58ff54b2464c - --env=PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin - --volume=/w/DEV/hotel-api-rs/db:/app/db - --network=bridge - --workdir=/app - -p 8080:8080 - --restart=no - --runtime=runc - -d rust-api:1.0.0 - - - - docker run ` - --name hotel-api ` - -e JWT_SECRET=your_jwt_secret_key ` - -v "/w/DEV/hotel-api-rs/db:/app/db" ` - -p 8080:8080 ` - rust-api:1.0.1 - -linux distro - - docker run ` - --name hotel-api ` - -e JWT_SECRET=your_jwt_secret_key ` - -v "/var/lib/hotel-api/db:/app/db" ` - -p 8080:8080 ` - rust-api:1.0.2 - - - - - - - - -BAD - - "Mounts": [ - { - "Type": "bind", - "Source": "/w/DEV/hotel-api-rs/db", - "Destination": "/app/db ", - "Mode": "", - "RW": true, - "Propagation": "rprivate" - } - ], - - -"Mounts": [ - { - "Type": "bind", - "Source": "/w/DEV/hotel-api-rs/db", - "Destination": "/app/db", - "Mode": "", - "RW": true, - "Propagation": "rprivate" - } - ], - - - "Mounts": [ - { - "Type": "bind", - "Source": "W:\\DEV\\hotel-api-rs\\db\\db.sqlite", - "Destination": "/app/db/db.sqlite", - "Mode": "", - "RW": true, - "Propagation": "rprivate" - } - ], - -GOOD - - "Mounts": [ - { - "Type": "bind", - "Source": "/w/DEV/hotel-api-rs/db", - "Destination": "/app/db", - "Mode": "", - "RW": true, - "Propagation": "rprivate" - } - ], - - - - -