commit 5d5e6393acf3fec4bc1fcc8412da6968f2d41cd4 Author: Martin Berg Alstad Date: Thu Aug 29 16:43:04 2024 +0200 First working API. Simple auth by creating sessions and storing in db diff --git a/.env b/.env new file mode 100644 index 0000000..fe143f5 --- /dev/null +++ b/.env @@ -0,0 +1 @@ +DATABASE_URL="postgres://postgres:postgres@localhost:32784/postgres" \ No newline at end of file diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..ea8c4bf --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +/target diff --git a/.idea/.gitignore b/.idea/.gitignore new file mode 100644 index 0000000..13566b8 --- /dev/null +++ b/.idea/.gitignore @@ -0,0 +1,8 @@ +# Default ignored files +/shelf/ +/workspace.xml +# Editor-based HTTP Client requests +/httpRequests/ +# Datasource local storage ignored files +/dataSources/ +/dataSources.local.xml diff --git a/.idea/dataSources.xml b/.idea/dataSources.xml new file mode 100644 index 0000000..cf1fc59 --- /dev/null +++ b/.idea/dataSources.xml @@ -0,0 +1,12 @@ + + + + + postgresql + true + org.postgresql.Driver + jdbc:postgresql://localhost:32784/postgres + $ProjectFileDir$ + + + \ No newline at end of file diff --git a/.idea/hotel_service.iml b/.idea/hotel_service.iml new file mode 100644 index 0000000..b319427 --- /dev/null +++ b/.idea/hotel_service.iml @@ -0,0 +1,15 @@ + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/.idea/modules.xml b/.idea/modules.xml new file mode 100644 index 0000000..ee95b3e --- /dev/null +++ b/.idea/modules.xml @@ -0,0 +1,8 @@ + + + + + + + + \ No newline at end of file diff --git a/.idea/sqldialects.xml b/.idea/sqldialects.xml new file mode 100644 index 0000000..84b8b5d --- /dev/null +++ b/.idea/sqldialects.xml @@ -0,0 +1,9 @@ + + + + + + + + + \ No newline at end of file diff --git a/.idea/vcs.xml b/.idea/vcs.xml new file mode 100644 index 0000000..35eb1dd --- /dev/null +++ b/.idea/vcs.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/Cargo.lock b/Cargo.lock new file mode 100644 index 0000000..68df004 --- /dev/null +++ b/Cargo.lock @@ -0,0 +1,3422 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 3 + +[[package]] +name = "addr2line" +version = "0.22.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e4503c46a5c0c7844e948c9a4d6acd9f50cccb4de1c48eb9e291ea17470c678" +dependencies = [ + "gimli", +] + +[[package]] +name = "adler" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe" + +[[package]] +name = "aho-corasick" +version = "1.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e60d3430d3a69478ad0993f19238d2df97c507009a52b3c10addcd7f6bcb916" +dependencies = [ + "memchr", +] + +[[package]] +name = "android-tzdata" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e999941b234f3131b00bc13c22d06e8c5ff726d1b6318ac7eb276997bbb4fef0" + +[[package]] +name = "android_system_properties" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311" +dependencies = [ + "libc", +] + +[[package]] +name = "arrayvec" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7c02d123df017efcdfbd739ef81735b36c5ba83ec3c59c80a9d7ecc718f92e50" + +[[package]] +name = "async-attributes" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a3203e79f4dd9bdda415ed03cf14dae5a2bf775c683a00f94e9cd1faf0f596e5" +dependencies = [ + "quote", + "syn 1.0.109", +] + +[[package]] +name = "async-channel" +version = "1.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "81953c529336010edd6d8e358f886d9581267795c61b19475b71314bffa46d35" +dependencies = [ + "concurrent-queue", + "event-listener 2.5.3", + "futures-core", +] + +[[package]] +name = "async-channel" +version = "2.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "89b47800b0be77592da0afd425cc03468052844aff33b84e33cc696f64e77b6a" +dependencies = [ + "concurrent-queue", + "event-listener-strategy", + "futures-core", + "pin-project-lite", +] + +[[package]] +name = "async-executor" +version = "1.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d7ebdfa2ebdab6b1760375fa7d6f382b9f486eac35fc994625a00e89280bdbb7" +dependencies = [ + "async-task", + "concurrent-queue", + "fastrand 2.1.1", + "futures-lite 2.3.0", + "slab", +] + +[[package]] +name = "async-global-executor" +version = "2.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05b1b633a2115cd122d73b955eadd9916c18c8f510ec9cd1686404c60ad1c29c" +dependencies = [ + "async-channel 2.3.1", + "async-executor", + "async-io 2.3.4", + "async-lock 3.4.0", + "blocking", + "futures-lite 2.3.0", + "once_cell", +] + +[[package]] +name = "async-io" +version = "1.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fc5b45d93ef0529756f812ca52e44c221b35341892d3dcc34132ac02f3dd2af" +dependencies = [ + "async-lock 2.8.0", + "autocfg", + "cfg-if", + "concurrent-queue", + "futures-lite 1.13.0", + "log", + "parking", + "polling 2.8.0", + "rustix 0.37.27", + "slab", + "socket2 0.4.10", + "waker-fn", +] + +[[package]] +name = "async-io" +version = "2.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "444b0228950ee6501b3568d3c93bf1176a1fdbc3b758dcd9475046d30f4dc7e8" +dependencies = [ + "async-lock 3.4.0", + "cfg-if", + "concurrent-queue", + "futures-io", + "futures-lite 2.3.0", + "parking", + "polling 3.7.3", + "rustix 0.38.34", + "slab", + "tracing", + "windows-sys 0.59.0", +] + +[[package]] +name = "async-lock" +version = "2.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "287272293e9d8c41773cec55e365490fe034813a2f172f502d6ddcf75b2f582b" +dependencies = [ + "event-listener 2.5.3", +] + +[[package]] +name = "async-lock" +version = "3.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff6e472cdea888a4bd64f342f09b3f50e1886d32afe8df3d663c01140b811b18" +dependencies = [ + "event-listener 5.3.1", + "event-listener-strategy", + "pin-project-lite", +] + +[[package]] +name = "async-std" +version = "1.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "62565bb4402e926b29953c785397c6dc0391b7b446e45008b0049eb43cec6f5d" +dependencies = [ + "async-attributes", + "async-channel 1.9.0", + "async-global-executor", + "async-io 1.13.0", + "async-lock 2.8.0", + "crossbeam-utils", + "futures-channel", + "futures-core", + "futures-io", + "futures-lite 1.13.0", + "gloo-timers", + "kv-log-macro", + "log", + "memchr", + "once_cell", + "pin-project-lite", + "pin-utils", + "slab", + "wasm-bindgen-futures", +] + +[[package]] +name = "async-task" +version = "4.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b75356056920673b02621b35afd0f7dda9306d03c79a30f5c56c44cf256e3de" + +[[package]] +name = "async-trait" +version = "0.1.81" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e0c28dcc82d7c8ead5cb13beb15405b57b8546e93215673ff8ca0349a028107" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.76", +] + +[[package]] +name = "atomic-waker" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0" + +[[package]] +name = "autocfg" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c4b4d0bd25bd0b74681c0ad21497610ce1b7c91b1022cd21c80c6fbdd9476b0" + +[[package]] +name = "axum" +version = "0.7.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3a6c9af12842a67734c9a2e355436e5d03b22383ed60cf13cd0c18fbfe3dcbcf" +dependencies = [ + "async-trait", + "axum-core", + "axum-macros", + "bytes", + "futures-util", + "http", + "http-body", + "http-body-util", + "hyper", + "hyper-util", + "itoa", + "matchit", + "memchr", + "mime", + "multer", + "percent-encoding", + "pin-project-lite", + "rustversion", + "serde", + "serde_json", + "serde_path_to_error", + "serde_urlencoded", + "sync_wrapper 1.0.1", + "tokio", + "tower 0.4.13", + "tower-layer", + "tower-service", + "tracing", +] + +[[package]] +name = "axum-core" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a15c63fd72d41492dc4f497196f5da1fb04fb7529e631d73630d1b491e47a2e3" +dependencies = [ + "async-trait", + "bytes", + "futures-util", + "http", + "http-body", + "http-body-util", + "mime", + "pin-project-lite", + "rustversion", + "sync_wrapper 0.1.2", + "tower-layer", + "tower-service", + "tracing", +] + +[[package]] +name = "axum-login" +version = "0.15.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4012877d9672b7902aa6567960208756f68a09de81e988fa18fe369e92f90471" +dependencies = [ + "async-trait", + "axum", + "form_urlencoded", + "serde", + "subtle", + "thiserror", + "tower-cookies", + "tower-layer", + "tower-service", + "tower-sessions", + "tracing", + "urlencoding", +] + +[[package]] +name = "axum-macros" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "00c055ee2d014ae5981ce1016374e8213682aa14d9bf40e48ab48b5f3ef20eaa" +dependencies = [ + "heck 0.4.1", + "proc-macro2", + "quote", + "syn 2.0.76", +] + +[[package]] +name = "axum-valid" +version = "0.19.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bf153a7451ae5606afd0262c06fc4d58517b70fdce82776e401281c76383170a" +dependencies = [ + "axum", + "validator", +] + +[[package]] +name = "backtrace" +version = "0.3.73" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5cc23269a4f8976d0a4d2e7109211a419fe30e8d88d677cd60b6bc79c5732e0a" +dependencies = [ + "addr2line", + "cc", + "cfg-if", + "libc", + "miniz_oxide", + "object", + "rustc-demangle", +] + +[[package]] +name = "base64" +version = "0.21.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d297deb1925b89f2ccc13d7635fa0714f12c87adce1c75356b39ca9b7178567" + +[[package]] +name = "base64" +version = "0.22.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" + +[[package]] +name = "base64ct" +version = "1.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8c3c1a368f70d6cf7302d78f8f7093da241fb8e8807c05cc9e51a125895a6d5b" + +[[package]] +name = "bitflags" +version = "1.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" + +[[package]] +name = "bitflags" +version = "2.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b048fb63fd8b5923fc5aa7b340d8e156aec7ec02f0c78fa8a6ddc2613f6f71de" + +[[package]] +name = "block-buffer" +version = "0.10.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" +dependencies = [ + "generic-array", +] + +[[package]] +name = "blocking" +version = "1.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "703f41c54fc768e63e091340b424302bb1c29ef4aa0c7f10fe849dfb114d29ea" +dependencies = [ + "async-channel 2.3.1", + "async-task", + "futures-io", + "futures-lite 2.3.0", + "piper", +] + +[[package]] +name = "bollard" +version = "0.17.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d41711ad46fda47cd701f6908e59d1bd6b9a2b7464c0d0aeab95c6d37096ff8a" +dependencies = [ + "base64 0.22.1", + "bollard-stubs", + "bytes", + "futures-core", + "futures-util", + "hex", + "home", + "http", + "http-body-util", + "hyper", + "hyper-named-pipe", + "hyper-rustls", + "hyper-util", + "hyperlocal", + "log", + "pin-project-lite", + "rustls", + "rustls-native-certs", + "rustls-pemfile", + "rustls-pki-types", + "serde", + "serde_derive", + "serde_json", + "serde_repr", + "serde_urlencoded", + "thiserror", + "tokio", + "tokio-util", + "tower-service", + "url", + "winapi", +] + +[[package]] +name = "bollard-stubs" +version = "1.45.0-rc.26.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6d7c5415e3a6bc6d3e99eff6268e488fd4ee25e7b28c10f08fa6760bd9de16e4" +dependencies = [ + "serde", + "serde_repr", + "serde_with", +] + +[[package]] +name = "bon" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ea8256e3cff531086cc3faf94c1649930ff64bceb2d0e8cc84fc0356d7ee9806" +dependencies = [ + "bon-macros", +] + +[[package]] +name = "bon-macros" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b99838f77c5073bc7846ecce92b64e7e5a5bd152a8ec392facf90ee4d90b4b35" +dependencies = [ + "darling", + "ident_case", + "proc-macro2", + "quote", + "syn 2.0.76", +] + +[[package]] +name = "bumpalo" +version = "3.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "79296716171880943b8470b5f8d03aa55eb2e645a4874bdbb28adb49162e012c" + +[[package]] +name = "byteorder" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" + +[[package]] +name = "bytes" +version = "1.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8318a53db07bb3f8dca91a600466bdb3f2eaadeedfdbcf02e1accbad9271ba50" + +[[package]] +name = "cc" +version = "1.1.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "50d2eb3cd3d1bf4529e31c215ee6f93ec5a3d536d9f578f93d9d33ee19562932" +dependencies = [ + "shlex", +] + +[[package]] +name = "cfg-if" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" + +[[package]] +name = "chrono" +version = "0.4.38" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a21f936df1771bf62b77f047b726c4625ff2e8aa607c01ec06e5a05bd8463401" +dependencies = [ + "android-tzdata", + "iana-time-zone", + "js-sys", + "num-traits", + "serde", + "wasm-bindgen", + "windows-targets 0.52.6", +] + +[[package]] +name = "concurrent-queue" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ca0197aee26d1ae37445ee532fefce43251d24cc7c166799f4d46817f1d3973" +dependencies = [ + "crossbeam-utils", +] + +[[package]] +name = "cookie" +version = "0.18.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ddef33a339a91ea89fb53151bd0a4689cfce27055c291dfa69945475d22c747" +dependencies = [ + "percent-encoding", + "time", + "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" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" + +[[package]] +name = "cpufeatures" +version = "0.2.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "51e852e6dc9a5bed1fae92dd2375037bf2b768725bf3be87811edee3249d09ad" +dependencies = [ + "libc", +] + +[[package]] +name = "crossbeam-utils" +version = "0.8.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "22ec99545bb0ed0ea7bb9b8e1e9122ea386ff8a48c0922e43f36d45ab09e0e80" + +[[package]] +name = "crypto-common" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3" +dependencies = [ + "generic-array", + "typenum", +] + +[[package]] +name = "darling" +version = "0.20.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6f63b86c8a8826a49b8c21f08a2d07338eec8d900540f8630dc76284be802989" +dependencies = [ + "darling_core", + "darling_macro", +] + +[[package]] +name = "darling_core" +version = "0.20.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "95133861a8032aaea082871032f5815eb9e98cef03fa916ab4500513994df9e5" +dependencies = [ + "fnv", + "ident_case", + "proc-macro2", + "quote", + "strsim 0.11.1", + "syn 2.0.76", +] + +[[package]] +name = "darling_macro" +version = "0.20.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d336a2a514f6ccccaa3e09b02d41d35330c07ddf03a62165fcec10bb561c7806" +dependencies = [ + "darling_core", + "quote", + "syn 2.0.76", +] + +[[package]] +name = "deadpool" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6541a3916932fe57768d4be0b1ffb5ec7cbf74ca8c903fdfd5c0fe8aa958f0ed" +dependencies = [ + "deadpool-runtime", + "num_cpus", + "tokio", +] + +[[package]] +name = "deadpool-diesel" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "590573e9e29c5190a5ff782136f871e6e652e35d598a349888e028693601adf1" +dependencies = [ + "deadpool", + "deadpool-sync", + "diesel", +] + +[[package]] +name = "deadpool-runtime" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "092966b41edc516079bdf31ec78a2e0588d1d0c08f78b91d8307215928642b2b" +dependencies = [ + "tokio", +] + +[[package]] +name = "deadpool-sync" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "524bc3df0d57e98ecd022e21ba31166c2625e7d3e5bcc4510efaeeab4abcab04" +dependencies = [ + "deadpool-runtime", +] + +[[package]] +name = "deluxe" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ed332aaf752b459088acf3dd4eca323e3ef4b83c70a84ca48fb0ec5305f1488" +dependencies = [ + "deluxe-core", + "deluxe-macros", + "once_cell", + "proc-macro2", + "syn 2.0.76", +] + +[[package]] +name = "deluxe-core" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eddada51c8576df9d6a8450c351ff63042b092c9458b8ac7d20f89cbd0ffd313" +dependencies = [ + "arrayvec", + "proc-macro2", + "quote", + "strsim 0.10.0", + "syn 2.0.76", +] + +[[package]] +name = "deluxe-macros" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f87546d9c837f0b7557e47b8bd6eae52c3c223141b76aa233c345c9ab41d9117" +dependencies = [ + "deluxe-core", + "heck 0.4.1", + "if_chain", + "proc-macro-crate 1.3.1", + "proc-macro2", + "quote", + "syn 2.0.76", +] + +[[package]] +name = "deranged" +version = "0.3.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b42b6fa04a440b495c8b04d0e71b707c585f83cb9cb28cf8cd0d976c315e31b4" +dependencies = [ + "powerfmt", + "serde", +] + +[[package]] +name = "derive_more" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4a9b99b9cbbe49445b21764dc0625032a89b145a2642e67603e1c936f5458d05" +dependencies = [ + "derive_more-impl", +] + +[[package]] +name = "derive_more-impl" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb7330aeadfbe296029522e6c40f315320aba36fc43a5b3632f3795348f3bd22" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.76", +] + +[[package]] +name = "diesel" +version = "2.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "65e13bab2796f412722112327f3e575601a3e9cdcbe426f0d30dbf43f3f5dc71" +dependencies = [ + "bitflags 2.6.0", + "byteorder", + "chrono", + "diesel_derives", + "itoa", + "pq-sys", + "serde_json", +] + +[[package]] +name = "diesel-async" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fcb799bb6f8ca6a794462125d7b8983b0c86e6c93a33a9c55934a4a5de4409d3" +dependencies = [ + "async-trait", + "deadpool", + "diesel", + "futures-util", + "scoped-futures", + "tokio", + "tokio-postgres", +] + +[[package]] +name = "diesel-crud-derive" +version = "0.1.0" +dependencies = [ + "deluxe", + "proc-macro2", + "quote", + "syn 2.0.76", +] + +[[package]] +name = "diesel-crud-trait" +version = "0.1.0" +dependencies = [ + "async-trait", + "deadpool-diesel", + "diesel", + "diesel-async", + "thiserror", +] + +[[package]] +name = "diesel_async_migrations" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b92dd60c54ac548b3a001a040e92abdca9b55c68136c8a641ef776dfeb133a52" +dependencies = [ + "diesel", + "diesel-async", + "diesel_async_migrations_macros", + "scoped-futures", + "tracing", +] + +[[package]] +name = "diesel_async_migrations_macros" +version = "0.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05de210f31e6ac18162501b03c37f839af9f9fd6dd6de2bb4031ae6691c47679" +dependencies = [ + "proc-macro2", + "quote", +] + +[[package]] +name = "diesel_derives" +version = "2.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7f2c3de51e2ba6bf2a648285696137aaf0f5f487bcbea93972fe8a364e131a4" +dependencies = [ + "diesel_table_macro_syntax", + "dsl_auto_type", + "proc-macro2", + "quote", + "syn 2.0.76", +] + +[[package]] +name = "diesel_table_macro_syntax" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "209c735641a413bc68c4923a9d6ad4bcb3ca306b794edaa7eb0b3228a99ffb25" +dependencies = [ + "syn 2.0.76", +] + +[[package]] +name = "digest" +version = "0.10.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" +dependencies = [ + "block-buffer", + "crypto-common", + "subtle", +] + +[[package]] +name = "dirs" +version = "5.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "44c45a9d03d6676652bcb5e724c7e988de1acad23a711b5217ab9cbecbec2225" +dependencies = [ + "dirs-sys", +] + +[[package]] +name = "dirs-sys" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "520f05a5cbd335fae5a99ff7a6ab8627577660ee5cfd6a94a6a929b52ff0321c" +dependencies = [ + "libc", + "option-ext", + "redox_users", + "windows-sys 0.48.0", +] + +[[package]] +name = "docker_credential" +version = "1.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "31951f49556e34d90ed28342e1df7e1cb7a229c4cab0aecc627b5d91edd41d07" +dependencies = [ + "base64 0.21.7", + "serde", + "serde_json", +] + +[[package]] +name = "dotenvy" +version = "0.15.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1aaf95b3e5c8f23aa320147307562d361db0ae0d51242340f558153b4eb2439b" + +[[package]] +name = "dotenvy_macro" +version = "0.15.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb0235d912a8c749f4e0c9f18ca253b4c28cfefc1d2518096016d6e3230b6424" +dependencies = [ + "dotenvy", + "proc-macro2", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "dsl_auto_type" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c5d9abe6314103864cc2d8901b7ae224e0ab1a103a0a416661b4097b0779b607" +dependencies = [ + "darling", + "either", + "heck 0.5.0", + "proc-macro2", + "quote", + "syn 2.0.76", +] + +[[package]] +name = "either" +version = "1.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "60b1af1c220855b6ceac025d3f6ecdd2b7c4894bfe9cd9bda4fbb4bc7c0d4cf0" + +[[package]] +name = "encoding_rs" +version = "0.8.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b45de904aa0b010bce2ab45264d0631681847fa7b6f2eaa7dab7619943bc4f59" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "equivalent" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5" + +[[package]] +name = "errno" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "534c5cf6194dfab3db3242765c03bbe257cf92f22b38f6bc0c58d59108a820ba" +dependencies = [ + "libc", + "windows-sys 0.52.0", +] + +[[package]] +name = "event-listener" +version = "2.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0206175f82b8d6bf6652ff7d71a1e27fd2e4efde587fd368662814d6ec1d9ce0" + +[[package]] +name = "event-listener" +version = "5.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6032be9bd27023a771701cc49f9f053c751055f71efb2e0ae5c15809093675ba" +dependencies = [ + "concurrent-queue", + "parking", + "pin-project-lite", +] + +[[package]] +name = "event-listener-strategy" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0f214dc438f977e6d4e3500aaa277f5ad94ca83fbbd9b1a15713ce2344ccc5a1" +dependencies = [ + "event-listener 5.3.1", + "pin-project-lite", +] + +[[package]] +name = "fallible-iterator" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4443176a9f2c162692bd3d352d745ef9413eec5782a80d8fd6f8a1ac692a07f7" + +[[package]] +name = "fastrand" +version = "1.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e51093e27b0797c359783294ca4f0a911c270184cb10f85783b118614a1501be" +dependencies = [ + "instant", +] + +[[package]] +name = "fastrand" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e8c02a5121d4ea3eb16a80748c74f5549a5665e4c21333c6098f283870fbdea6" + +[[package]] +name = "fnv" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" + +[[package]] +name = "form_urlencoded" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e13624c2627564efccf4934284bdd98cbaa14e79b0b5a141218e507b3a823456" +dependencies = [ + "percent-encoding", +] + +[[package]] +name = "futures" +version = "0.3.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "645c6916888f6cb6350d2550b80fb63e734897a8498abe35cfb732b6487804b0" +dependencies = [ + "futures-channel", + "futures-core", + "futures-executor", + "futures-io", + "futures-sink", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-channel" +version = "0.3.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eac8f7d7865dcb88bd4373ab671c8cf4508703796caa2b1985a9ca867b3fcb78" +dependencies = [ + "futures-core", + "futures-sink", +] + +[[package]] +name = "futures-core" +version = "0.3.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dfc6580bb841c5a68e9ef15c77ccc837b40a7504914d52e47b8b0e9bbda25a1d" + +[[package]] +name = "futures-executor" +version = "0.3.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a576fc72ae164fca6b9db127eaa9a9dda0d61316034f33a0a0d4eda41f02b01d" +dependencies = [ + "futures-core", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-io" +version = "0.3.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a44623e20b9681a318efdd71c299b6b222ed6f231972bfe2f224ebad6311f0c1" + +[[package]] +name = "futures-lite" +version = "1.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "49a9d51ce47660b1e808d3c990b4709f2f415d928835a17dfd16991515c46bce" +dependencies = [ + "fastrand 1.9.0", + "futures-core", + "futures-io", + "memchr", + "parking", + "pin-project-lite", + "waker-fn", +] + +[[package]] +name = "futures-lite" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52527eb5074e35e9339c6b4e8d12600c7128b68fb25dcb9fa9dec18f7c25f3a5" +dependencies = [ + "fastrand 2.1.1", + "futures-core", + "futures-io", + "parking", + "pin-project-lite", +] + +[[package]] +name = "futures-macro" +version = "0.3.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "87750cf4b7a4c0625b1529e4c543c2182106e4dedc60a2a6455e00d212c489ac" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.76", +] + +[[package]] +name = "futures-sink" +version = "0.3.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9fb8e00e87438d937621c1c6269e53f536c14d3fbd6a042bb24879e57d474fb5" + +[[package]] +name = "futures-task" +version = "0.3.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38d84fa142264698cdce1a9f9172cf383a0c82de1bddcf3092901442c4097004" + +[[package]] +name = "futures-timer" +version = "3.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f288b0a4f20f9a56b5d1da57e2227c661b7b16168e2f72365f57b63326e29b24" + +[[package]] +name = "futures-util" +version = "0.3.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d6401deb83407ab3da39eba7e33987a73c3df0c82b4bb5813ee871c19c41d48" +dependencies = [ + "futures-channel", + "futures-core", + "futures-io", + "futures-macro", + "futures-sink", + "futures-task", + "memchr", + "pin-project-lite", + "pin-utils", + "slab", +] + +[[package]] +name = "generic-array" +version = "0.14.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" +dependencies = [ + "typenum", + "version_check", +] + +[[package]] +name = "getrandom" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c4567c8db10ae91089c99af84c68c38da3ec2f087c3f82960bcdbf3656b6f4d7" +dependencies = [ + "cfg-if", + "libc", + "wasi", +] + +[[package]] +name = "gimli" +version = "0.29.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "40ecd4077b5ae9fd2e9e169b102c6c330d0605168eb0e8bf79952b256dbefffd" + +[[package]] +name = "glob" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d2fabcfbdc87f4758337ca535fb41a6d701b65693ce38287d856d1674551ec9b" + +[[package]] +name = "gloo-timers" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b995a66bb87bebce9a0f4a95aed01daca4872c050bfcb21653361c03bc35e5c" +dependencies = [ + "futures-channel", + "futures-core", + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "hashbrown" +version = "0.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" + +[[package]] +name = "hashbrown" +version = "0.14.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1" + +[[package]] +name = "heck" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8" + +[[package]] +name = "heck" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" + +[[package]] +name = "hermit-abi" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d231dfb89cfffdbc30e7fc41579ed6066ad03abda9e567ccafae602b97ec5024" + +[[package]] +name = "hermit-abi" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fbf6a919d6cf397374f7dfeeea91d974c7c0a7221d0d0f4f20d859d329e53fcc" + +[[package]] +name = "hex" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" + +[[package]] +name = "hmac" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c49c37c09c17a53d937dfbb742eb3a961d65a994e6bcdcf37e7399d0cc8ab5e" +dependencies = [ + "digest", +] + +[[package]] +name = "home" +version = "0.5.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3d1354bf6b7235cb4a0576c2619fd4ed18183f689b12b006a0ee7329eeff9a5" +dependencies = [ + "windows-sys 0.52.0", +] + +[[package]] +name = "hotel_service" +version = "0.1.0" +dependencies = [ + "async-std", + "axum", + "axum-login", + "axum-valid", + "base64ct", + "bon", + "chrono", + "deadpool-diesel", + "derive_more", + "diesel", + "diesel-async", + "diesel_async_migrations", + "digest", + "dotenvy_macro", + "futures", + "hyper", + "lib", + "mime", + "rand", + "rand_chacha", + "rstest", + "secrecy", + "serde", + "serde_json", + "sha2", + "strum", + "testcontainers-modules", + "thiserror", + "tokio", + "tower 0.5.0", + "tower-sessions", + "validator", +] + +[[package]] +name = "http" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "21b9ddb458710bc376481b842f5da65cdf31522de232c1ca8146abce2a358258" +dependencies = [ + "bytes", + "fnv", + "itoa", +] + +[[package]] +name = "http-body" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1efedce1fb8e6913f23e0c92de8e62cd5b772a67e7b3946df930a62566c93184" +dependencies = [ + "bytes", + "http", +] + +[[package]] +name = "http-body-util" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "793429d76616a256bcb62c2a2ec2bed781c8307e797e2598c50010f2bee2544f" +dependencies = [ + "bytes", + "futures-util", + "http", + "http-body", + "pin-project-lite", +] + +[[package]] +name = "httparse" +version = "1.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fcc0b4a115bf80b728eb8ea024ad5bd707b615bfed49e0665b6e0f86fd082d9" + +[[package]] +name = "httpdate" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9" + +[[package]] +name = "hyper" +version = "1.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "50dfd22e0e76d0f662d429a5f80fcaf3855009297eab6a0a9f8543834744ba05" +dependencies = [ + "bytes", + "futures-channel", + "futures-util", + "http", + "http-body", + "httparse", + "httpdate", + "itoa", + "pin-project-lite", + "smallvec", + "tokio", + "want", +] + +[[package]] +name = "hyper-named-pipe" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "73b7d8abf35697b81a825e386fc151e0d503e8cb5fcb93cc8669c376dfd6f278" +dependencies = [ + "hex", + "hyper", + "hyper-util", + "pin-project-lite", + "tokio", + "tower-service", + "winapi", +] + +[[package]] +name = "hyper-rustls" +version = "0.27.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5ee4be2c948921a1a5320b629c4193916ed787a7f7f293fd3f7f5a6c9de74155" +dependencies = [ + "futures-util", + "http", + "hyper", + "hyper-util", + "rustls", + "rustls-pki-types", + "tokio", + "tokio-rustls", + "tower-service", +] + +[[package]] +name = "hyper-util" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cde7055719c54e36e95e8719f95883f22072a48ede39db7fc17a4e1d5281e9b9" +dependencies = [ + "bytes", + "futures-channel", + "futures-util", + "http", + "http-body", + "hyper", + "pin-project-lite", + "socket2 0.5.7", + "tokio", + "tower 0.4.13", + "tower-service", + "tracing", +] + +[[package]] +name = "hyperlocal" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "986c5ce3b994526b3cd75578e62554abd09f0899d6206de48b3e96ab34ccc8c7" +dependencies = [ + "hex", + "http-body-util", + "hyper", + "hyper-util", + "pin-project-lite", + "tokio", + "tower-service", +] + +[[package]] +name = "iana-time-zone" +version = "0.1.60" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7ffbb5a1b541ea2561f8c41c087286cc091e21e556a4f09a8f6cbf17b69b141" +dependencies = [ + "android_system_properties", + "core-foundation-sys", + "iana-time-zone-haiku", + "js-sys", + "wasm-bindgen", + "windows-core", +] + +[[package]] +name = "iana-time-zone-haiku" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f" +dependencies = [ + "cc", +] + +[[package]] +name = "ident_case" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39" + +[[package]] +name = "idna" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "634d9b1461af396cad843f47fdba5597a4f9e6ddd4bfb6ff5d85028c25cb12f6" +dependencies = [ + "unicode-bidi", + "unicode-normalization", +] + +[[package]] +name = "if_chain" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb56e1aa765b4b4f3aadfab769793b7087bb03a4ea4920644a6d238e2df5b9ed" + +[[package]] +name = "indexmap" +version = "1.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bd070e393353796e801d209ad339e89596eb4c8d430d18ede6a1cced8fafbd99" +dependencies = [ + "autocfg", + "hashbrown 0.12.3", + "serde", +] + +[[package]] +name = "indexmap" +version = "2.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93ead53efc7ea8ed3cfb0c79fc8023fbb782a5432b52830b6518941cebe6505c" +dependencies = [ + "equivalent", + "hashbrown 0.14.5", + "serde", +] + +[[package]] +name = "instant" +version = "0.1.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e0242819d153cba4b4b05a5a8f2a7e9bbf97b6055b2a002b395c96b5ff3c0222" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "into-response-derive" +version = "1.1.0" +dependencies = [ + "quote", + "syn 2.0.76", +] + +[[package]] +name = "io-lifetimes" +version = "1.0.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eae7b9aee968036d54dce06cebaefd919e4472e753296daccd6d344e3e2df0c2" +dependencies = [ + "hermit-abi 0.3.9", + "libc", + "windows-sys 0.48.0", +] + +[[package]] +name = "itoa" +version = "1.0.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "49f1f14873335454500d59611f1cf4a4b0f786f9ac11f4312a78e4cf2566695b" + +[[package]] +name = "js-sys" +version = "0.3.70" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1868808506b929d7b0cfa8f75951347aa71bb21144b7791bae35d9bccfcfe37a" +dependencies = [ + "wasm-bindgen", +] + +[[package]] +name = "kv-log-macro" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0de8b303297635ad57c9f5059fd9cee7a47f8e8daa09df0fcd07dd39fb22977f" +dependencies = [ + "log", +] + +[[package]] +name = "lazy_static" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" + +[[package]] +name = "lib" +version = "1.4.3" +dependencies = [ + "axum", + "chrono", + "derive_more", + "diesel-crud-derive", + "diesel-crud-trait", + "into-response-derive", + "mime", + "serde", + "thiserror", + "tokio", + "tower 0.5.0", + "tower-http", + "tracing", + "tracing-subscriber", +] + +[[package]] +name = "libc" +version = "0.2.158" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d8adc4bb1803a324070e64a98ae98f38934d91957a99cfb3a43dcbc01bc56439" + +[[package]] +name = "libredox" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c0ff37bd590ca25063e35af745c343cb7a0271906fb7b37e4813e8f79f00268d" +dependencies = [ + "bitflags 2.6.0", + "libc", +] + +[[package]] +name = "linux-raw-sys" +version = "0.3.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef53942eb7bf7ff43a617b3e2c1c4a5ecf5944a7c1bc12d7ee39bbb15e5c1519" + +[[package]] +name = "linux-raw-sys" +version = "0.4.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78b3ae25bc7c8c38cec158d1f2757ee79e9b3740fbc7ccf0e59e4b08d793fa89" + +[[package]] +name = "lock_api" +version = "0.4.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "07af8b9cdd281b7915f413fa73f29ebd5d55d0d3f0155584dade1ff18cea1b17" +dependencies = [ + "autocfg", + "scopeguard", + "serde", +] + +[[package]] +name = "log" +version = "0.4.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a7a70ba024b9dc04c27ea2f0c0548feb474ec5c54bba33a7f72f873a39d07b24" +dependencies = [ + "value-bag", +] + +[[package]] +name = "matchit" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0e7465ac9959cc2b1404e8e2367b43684a6d13790fe23056cc8c6c5a6b7bcb94" + +[[package]] +name = "md-5" +version = "0.10.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d89e7ee0cfbedfc4da3340218492196241d89eefb6dab27de5df917a6d2e78cf" +dependencies = [ + "cfg-if", + "digest", +] + +[[package]] +name = "memchr" +version = "2.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3" + +[[package]] +name = "mime" +version = "0.3.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" + +[[package]] +name = "miniz_oxide" +version = "0.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8a240ddb74feaf34a79a7add65a741f3167852fba007066dcac1ca548d89c08" +dependencies = [ + "adler", +] + +[[package]] +name = "mio" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "80e04d1dcff3aae0704555fe5fee3bcfaf3d1fdf8a7e521d5b9d2b42acb52cec" +dependencies = [ + "hermit-abi 0.3.9", + "libc", + "wasi", + "windows-sys 0.52.0", +] + +[[package]] +name = "multer" +version = "3.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "83e87776546dc87511aa5ee218730c92b666d7264ab6ed41f9d215af9cd5224b" +dependencies = [ + "bytes", + "encoding_rs", + "futures-util", + "http", + "httparse", + "memchr", + "mime", + "spin", + "version_check", +] + +[[package]] +name = "nu-ansi-term" +version = "0.46.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77a8165726e8236064dbb45459242600304b42a5ea24ee2948e18e023bf7ba84" +dependencies = [ + "overload", + "winapi", +] + +[[package]] +name = "num-conv" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "51d515d32fb182ee37cda2ccdcb92950d6a3c2893aa280e540671c2cd0f3b1d9" + +[[package]] +name = "num-traits" +version = "0.2.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" +dependencies = [ + "autocfg", +] + +[[package]] +name = "num_cpus" +version = "1.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4161fcb6d602d4d2081af7c3a45852d875a03dd337a6bfdd6e06407b61342a43" +dependencies = [ + "hermit-abi 0.3.9", + "libc", +] + +[[package]] +name = "object" +version = "0.36.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "27b64972346851a39438c60b341ebc01bba47464ae329e55cf343eb93964efd9" +dependencies = [ + "memchr", +] + +[[package]] +name = "once_cell" +version = "1.19.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3fdb12b2476b595f9358c5161aa467c2438859caa136dec86c26fdd2efe17b92" + +[[package]] +name = "openssl-probe" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff011a302c396a5197692431fc1948019154afc178baf7d8e37367442a4601cf" + +[[package]] +name = "option-ext" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "04744f49eae99ab78e0d5c0b603ab218f515ea8cfe5a456d7629ad883a3b6e7d" + +[[package]] +name = "overload" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b15813163c1d831bf4a13c3610c05c0d03b39feb07f7e09fa234dac9b15aaf39" + +[[package]] +name = "parking" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb813b8af86854136c6922af0598d719255ecb2179515e6e7730d468f05c9cae" + +[[package]] +name = "parking_lot" +version = "0.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1bf18183cf54e8d6059647fc3063646a1801cf30896933ec2311622cc4b9a27" +dependencies = [ + "lock_api", + "parking_lot_core", +] + +[[package]] +name = "parking_lot_core" +version = "0.9.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e401f977ab385c9e4e3ab30627d6f26d00e2c73eef317493c4ec6d468726cf8" +dependencies = [ + "cfg-if", + "libc", + "redox_syscall 0.5.3", + "smallvec", + "windows-targets 0.52.6", +] + +[[package]] +name = "parse-display" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "914a1c2265c98e2446911282c6ac86d8524f495792c38c5bd884f80499c7538a" +dependencies = [ + "parse-display-derive", + "regex", + "regex-syntax", +] + +[[package]] +name = "parse-display-derive" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2ae7800a4c974efd12df917266338e79a7a74415173caf7e70aa0a0707345281" +dependencies = [ + "proc-macro2", + "quote", + "regex", + "regex-syntax", + "structmeta", + "syn 2.0.76", +] + +[[package]] +name = "percent-encoding" +version = "2.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3148f5046208a5d56bcfc03053e3ca6334e51da8dfb19b6cdc8b306fae3283e" + +[[package]] +name = "phf" +version = "0.11.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ade2d8b8f33c7333b51bcf0428d37e217e9f32192ae4772156f65063b8ce03dc" +dependencies = [ + "phf_shared", +] + +[[package]] +name = "phf_shared" +version = "0.11.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "90fcb95eef784c2ac79119d1dd819e162b5da872ce6f3c3abe1e8ca1c082f72b" +dependencies = [ + "siphasher", +] + +[[package]] +name = "pin-project" +version = "1.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6bf43b791c5b9e34c3d182969b4abb522f9343702850a2e57f460d00d09b4b3" +dependencies = [ + "pin-project-internal", +] + +[[package]] +name = "pin-project-internal" +version = "1.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2f38a4412a78282e09a2cf38d195ea5420d15ba0602cb375210efbc877243965" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.76", +] + +[[package]] +name = "pin-project-lite" +version = "0.2.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bda66fc9667c18cb2758a2ac84d1167245054bcf85d5d1aaa6923f45801bdd02" + +[[package]] +name = "pin-utils" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" + +[[package]] +name = "piper" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96c8c490f422ef9a4efd2cb5b42b76c8613d7e7dfc1caf667b8a3350a5acc066" +dependencies = [ + "atomic-waker", + "fastrand 2.1.1", + "futures-io", +] + +[[package]] +name = "polling" +version = "2.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4b2d323e8ca7996b3e23126511a523f7e62924d93ecd5ae73b333815b0eb3dce" +dependencies = [ + "autocfg", + "bitflags 1.3.2", + "cfg-if", + "concurrent-queue", + "libc", + "log", + "pin-project-lite", + "windows-sys 0.48.0", +] + +[[package]] +name = "polling" +version = "3.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cc2790cd301dec6cd3b7a025e4815cf825724a51c98dccfe6a3e55f05ffb6511" +dependencies = [ + "cfg-if", + "concurrent-queue", + "hermit-abi 0.4.0", + "pin-project-lite", + "rustix 0.38.34", + "tracing", + "windows-sys 0.59.0", +] + +[[package]] +name = "postgres-protocol" +version = "0.6.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "acda0ebdebc28befa84bee35e651e4c5f09073d668c7aed4cf7e23c3cda84b23" +dependencies = [ + "base64 0.22.1", + "byteorder", + "bytes", + "fallible-iterator", + "hmac", + "md-5", + "memchr", + "rand", + "sha2", + "stringprep", +] + +[[package]] +name = "postgres-types" +version = "0.2.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "02048d9e032fb3cc3413bbf7b83a15d84a5d419778e2628751896d856498eee9" +dependencies = [ + "bytes", + "fallible-iterator", + "postgres-protocol", +] + +[[package]] +name = "powerfmt" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" + +[[package]] +name = "ppv-lite86" +version = "0.2.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77957b295656769bb8ad2b6a6b09d897d94f05c41b069aede1fcdaa675eaea04" +dependencies = [ + "zerocopy", +] + +[[package]] +name = "pq-sys" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a24ff9e4cf6945c988f0db7005d87747bf72864965c3529d259ad155ac41d584" +dependencies = [ + "vcpkg", +] + +[[package]] +name = "proc-macro-crate" +version = "1.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f4c021e1093a56626774e81216a4ce732a735e5bad4868a03f3ed65ca0c3919" +dependencies = [ + "once_cell", + "toml_edit 0.19.15", +] + +[[package]] +name = "proc-macro-crate" +version = "3.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6d37c51ca738a55da99dc0c4a34860fd675453b8b36209178c2249bb13651284" +dependencies = [ + "toml_edit 0.21.1", +] + +[[package]] +name = "proc-macro-error" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da25490ff9892aab3fcf7c36f08cfb902dd3e71ca0f9f9517bea02a73a5ce38c" +dependencies = [ + "proc-macro-error-attr", + "proc-macro2", + "quote", + "syn 1.0.109", + "version_check", +] + +[[package]] +name = "proc-macro-error-attr" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1be40180e52ecc98ad80b184934baf3d0d29f979574e439af5a55274b35f869" +dependencies = [ + "proc-macro2", + "quote", + "version_check", +] + +[[package]] +name = "proc-macro2" +version = "1.0.86" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e719e8df665df0d1c8fbfd238015744736151d4445ec0836b8e628aae103b77" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quote" +version = "1.0.37" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b5b9d34b8991d19d98081b46eacdd8eb58c6f2b201139f7c5f643cc155a633af" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "rand" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" +dependencies = [ + "libc", + "rand_chacha", + "rand_core", +] + +[[package]] +name = "rand_chacha" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" +dependencies = [ + "ppv-lite86", + "rand_core", +] + +[[package]] +name = "rand_core" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" +dependencies = [ + "getrandom", +] + +[[package]] +name = "redox_syscall" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4722d768eff46b75989dd134e5c353f0d6296e5aaa3132e776cbdb56be7731aa" +dependencies = [ + "bitflags 1.3.2", +] + +[[package]] +name = "redox_syscall" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2a908a6e00f1fdd0dfd9c0eb08ce85126f6d8bbda50017e74bc4a4b7d4a926a4" +dependencies = [ + "bitflags 2.6.0", +] + +[[package]] +name = "redox_users" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba009ff324d1fc1b900bd1fdb31564febe58a8ccc8a6fdbb93b543d33b13ca43" +dependencies = [ + "getrandom", + "libredox", + "thiserror", +] + +[[package]] +name = "regex" +version = "1.10.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4219d74c6b67a3654a9fbebc4b419e22126d13d2f3c4a07ee0cb61ff79a79619" +dependencies = [ + "aho-corasick", + "memchr", + "regex-automata", + "regex-syntax", +] + +[[package]] +name = "regex-automata" +version = "0.4.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38caf58cc5ef2fed281f89292ef23f6365465ed9a41b7a7754eb4e26496c92df" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax", +] + +[[package]] +name = "regex-syntax" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a66a03ae7c801facd77a29370b4faec201768915ac14a721ba36f20bc9c209b" + +[[package]] +name = "relative-path" +version = "1.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba39f3699c378cd8970968dcbff9c43159ea4cfbd88d43c00b22f2ef10a435d2" + +[[package]] +name = "ring" +version = "0.17.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c17fa4cb658e3583423e915b9f3acc01cceaee1860e33d59ebae66adc3a2dc0d" +dependencies = [ + "cc", + "cfg-if", + "getrandom", + "libc", + "spin", + "untrusted", + "windows-sys 0.52.0", +] + +[[package]] +name = "rstest" +version = "0.22.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7b423f0e62bdd61734b67cd21ff50871dfaeb9cc74f869dcd6af974fbcb19936" +dependencies = [ + "futures", + "futures-timer", + "rstest_macros", + "rustc_version", +] + +[[package]] +name = "rstest_macros" +version = "0.22.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c5e1711e7d14f74b12a58411c542185ef7fb7f2e7f8ee6e2940a883628522b42" +dependencies = [ + "cfg-if", + "glob", + "proc-macro-crate 3.1.0", + "proc-macro2", + "quote", + "regex", + "relative-path", + "rustc_version", + "syn 2.0.76", + "unicode-ident", +] + +[[package]] +name = "rustc-demangle" +version = "0.1.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "719b953e2095829ee67db738b3bfa9fa368c94900df327b3f07fe6e794d2fe1f" + +[[package]] +name = "rustc_version" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bfa0f585226d2e68097d4f95d113b15b83a82e819ab25717ec0590d9584ef366" +dependencies = [ + "semver", +] + +[[package]] +name = "rustix" +version = "0.37.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fea8ca367a3a01fe35e6943c400addf443c0f57670e6ec51196f71a4b8762dd2" +dependencies = [ + "bitflags 1.3.2", + "errno", + "io-lifetimes", + "libc", + "linux-raw-sys 0.3.8", + "windows-sys 0.48.0", +] + +[[package]] +name = "rustix" +version = "0.38.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "70dc5ec042f7a43c4a73241207cecc9873a06d45debb38b329f8541d85c2730f" +dependencies = [ + "bitflags 2.6.0", + "errno", + "libc", + "linux-raw-sys 0.4.14", + "windows-sys 0.52.0", +] + +[[package]] +name = "rustls" +version = "0.23.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c58f8c84392efc0a126acce10fa59ff7b3d2ac06ab451a33f2741989b806b044" +dependencies = [ + "once_cell", + "ring", + "rustls-pki-types", + "rustls-webpki", + "subtle", + "zeroize", +] + +[[package]] +name = "rustls-native-certs" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e5bfb394eeed242e909609f56089eecfe5fda225042e8b171791b9c95f5931e5" +dependencies = [ + "openssl-probe", + "rustls-pemfile", + "rustls-pki-types", + "schannel", + "security-framework", +] + +[[package]] +name = "rustls-pemfile" +version = "2.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "196fe16b00e106300d3e45ecfcb764fa292a535d7326a29a5875c579c7417425" +dependencies = [ + "base64 0.22.1", + "rustls-pki-types", +] + +[[package]] +name = "rustls-pki-types" +version = "1.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc0a2ce646f8655401bb81e7927b812614bd5d91dbc968696be50603510fcaf0" + +[[package]] +name = "rustls-webpki" +version = "0.102.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "84678086bd54edf2b415183ed7a94d0efb049f1b646a33e22a36f3794be6ae56" +dependencies = [ + "ring", + "rustls-pki-types", + "untrusted", +] + +[[package]] +name = "rustversion" +version = "1.0.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "955d28af4278de8121b7ebeb796b6a45735dc01436d898801014aced2773a3d6" + +[[package]] +name = "ryu" +version = "1.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f3cb5ba0dc43242ce17de99c180e96db90b235b8a9fdc9543c96d2209116bd9f" + +[[package]] +name = "schannel" +version = "0.1.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fbc91545643bcf3a0bbb6569265615222618bdf33ce4ffbbd13c4bbd4c093534" +dependencies = [ + "windows-sys 0.52.0", +] + +[[package]] +name = "scoped-futures" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b1473e24c637950c9bd38763220bea91ec3e095a89f672bbd7a10d03e77ba467" +dependencies = [ + "cfg-if", + "pin-utils", +] + +[[package]] +name = "scopeguard" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" + +[[package]] +name = "secrecy" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9bd1c54ea06cfd2f6b63219704de0b9b4f72dcc2b8fdef820be6cd799780e91e" +dependencies = [ + "serde", + "zeroize", +] + +[[package]] +name = "security-framework" +version = "2.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "897b2245f0b511c87893af39b033e5ca9cce68824c4d7e7630b5a1d339658d02" +dependencies = [ + "bitflags 2.6.0", + "core-foundation", + "core-foundation-sys", + "libc", + "security-framework-sys", +] + +[[package]] +name = "security-framework-sys" +version = "2.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75da29fe9b9b08fe9d6b22b5b4bcbc75d8db3aa31e639aa56bb62e9d46bfceaf" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "semver" +version = "1.0.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "61697e0a1c7e512e84a621326239844a24d8207b4669b41bc18b32ea5cbf988b" + +[[package]] +name = "serde" +version = "1.0.209" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "99fce0ffe7310761ca6bf9faf5115afbc19688edd00171d81b1bb1b116c63e09" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.209" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a5831b979fd7b5439637af1752d535ff49f4860c0f341d1baeb6faf0f4242170" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.76", +] + +[[package]] +name = "serde_json" +version = "1.0.127" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8043c06d9f82bd7271361ed64f415fe5e12a77fdb52e573e7f06a516dea329ad" +dependencies = [ + "itoa", + "memchr", + "ryu", + "serde", +] + +[[package]] +name = "serde_path_to_error" +version = "0.1.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "af99884400da37c88f5e9146b7f1fd0fbcae8f6eec4e9da38b67d05486f814a6" +dependencies = [ + "itoa", + "serde", +] + +[[package]] +name = "serde_repr" +version = "0.1.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c64451ba24fc7a6a2d60fc75dd9c83c90903b19028d4eff35e88fc1e86564e9" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.76", +] + +[[package]] +name = "serde_urlencoded" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3491c14715ca2294c4d6a88f15e84739788c1d030eed8c110436aafdaa2f3fd" +dependencies = [ + "form_urlencoded", + "itoa", + "ryu", + "serde", +] + +[[package]] +name = "serde_with" +version = "3.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "69cecfa94848272156ea67b2b1a53f20fc7bc638c4a46d2f8abde08f05f4b857" +dependencies = [ + "base64 0.22.1", + "chrono", + "hex", + "indexmap 1.9.3", + "indexmap 2.4.0", + "serde", + "serde_derive", + "serde_json", + "serde_with_macros", + "time", +] + +[[package]] +name = "serde_with_macros" +version = "3.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a8fee4991ef4f274617a51ad4af30519438dacb2f56ac773b08a1922ff743350" +dependencies = [ + "darling", + "proc-macro2", + "quote", + "syn 2.0.76", +] + +[[package]] +name = "sha2" +version = "0.10.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "793db75ad2bcafc3ffa7c68b215fee268f537982cd901d132f89c6343f3a3dc8" +dependencies = [ + "cfg-if", + "cpufeatures", + "digest", +] + +[[package]] +name = "sharded-slab" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f40ca3c46823713e0d4209592e8d6e826aa57e928f09752619fc696c499637f6" +dependencies = [ + "lazy_static", +] + +[[package]] +name = "shlex" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" + +[[package]] +name = "siphasher" +version = "0.3.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38b58827f4464d87d377d175e90bf58eb00fd8716ff0a62f80356b5e61555d0d" + +[[package]] +name = "slab" +version = "0.4.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f92a496fb766b417c996b9c5e57daf2f7ad3b0bebe1ccfca4856390e3d3bb67" +dependencies = [ + "autocfg", +] + +[[package]] +name = "smallvec" +version = "1.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c5e1a9a646d36c3599cd173a41282daf47c44583ad367b8e6837255952e5c67" + +[[package]] +name = "socket2" +version = "0.4.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f7916fc008ca5542385b89a3d3ce689953c143e9304a9bf8beec1de48994c0d" +dependencies = [ + "libc", + "winapi", +] + +[[package]] +name = "socket2" +version = "0.5.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ce305eb0b4296696835b71df73eb912e0f1ffd2556a501fcede6e0c50349191c" +dependencies = [ + "libc", + "windows-sys 0.52.0", +] + +[[package]] +name = "spin" +version = "0.9.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6980e8d7511241f8acf4aebddbb1ff938df5eebe98691418c4468d0b72a96a67" + +[[package]] +name = "stringprep" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7b4df3d392d81bd458a8a621b8bffbd2302a12ffe288a9d931670948749463b1" +dependencies = [ + "unicode-bidi", + "unicode-normalization", + "unicode-properties", +] + +[[package]] +name = "strsim" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623" + +[[package]] +name = "strsim" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" + +[[package]] +name = "structmeta" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2e1575d8d40908d70f6fd05537266b90ae71b15dbbe7a8b7dffa2b759306d329" +dependencies = [ + "proc-macro2", + "quote", + "structmeta-derive", + "syn 2.0.76", +] + +[[package]] +name = "structmeta-derive" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "152a0b65a590ff6c3da95cabe2353ee04e6167c896b28e3b14478c2636c922fc" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.76", +] + +[[package]] +name = "strum" +version = "0.26.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8fec0f0aef304996cf250b31b5a10dee7980c85da9d759361292b8bca5a18f06" +dependencies = [ + "strum_macros", +] + +[[package]] +name = "strum_macros" +version = "0.26.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4c6bee85a5a24955dc440386795aa378cd9cf82acd5f764469152d2270e581be" +dependencies = [ + "heck 0.5.0", + "proc-macro2", + "quote", + "rustversion", + "syn 2.0.76", +] + +[[package]] +name = "subtle" +version = "2.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" + +[[package]] +name = "syn" +version = "1.0.109" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "syn" +version = "2.0.76" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "578e081a14e0cefc3279b0472138c513f37b41a08d5a3cca9b6e4e8ceb6cd525" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "sync_wrapper" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2047c6ded9c721764247e62cd3b03c09ffc529b2ba5b10ec482ae507a4a70160" + +[[package]] +name = "sync_wrapper" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a7065abeca94b6a8a577f9bd45aa0867a2238b74e8eb67cf10d492bc39351394" + +[[package]] +name = "testcontainers" +version = "0.21.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5f7d80fe0008971413157e67062150cbf508b92f0eb525b9f49de1aec4267f24" +dependencies = [ + "async-trait", + "bollard", + "bollard-stubs", + "bytes", + "dirs", + "docker_credential", + "either", + "futures", + "log", + "memchr", + "parse-display", + "pin-project-lite", + "serde", + "serde_json", + "serde_with", + "thiserror", + "tokio", + "tokio-stream", + "tokio-util", + "url", +] + +[[package]] +name = "testcontainers-modules" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "868e8e818fe37b8ed4c21ac72185206b48e8767b5ad3836d7ec0e5c9386e19a2" +dependencies = [ + "testcontainers", +] + +[[package]] +name = "thiserror" +version = "1.0.63" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c0342370b38b6a11b6cc11d6a805569958d54cfa061a29969c3b5ce2ea405724" +dependencies = [ + "thiserror-impl", +] + +[[package]] +name = "thiserror-impl" +version = "1.0.63" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4558b58466b9ad7ca0f102865eccc95938dca1a74a856f2b57b6629050da261" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.76", +] + +[[package]] +name = "thread_local" +version = "1.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b9ef9bad013ada3808854ceac7b46812a6465ba368859a37e2100283d2d719c" +dependencies = [ + "cfg-if", + "once_cell", +] + +[[package]] +name = "time" +version = "0.3.36" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5dfd88e563464686c916c7e46e623e520ddc6d79fa6641390f2e3fa86e83e885" +dependencies = [ + "deranged", + "itoa", + "num-conv", + "powerfmt", + "serde", + "time-core", + "time-macros", +] + +[[package]] +name = "time-core" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef927ca75afb808a4d64dd374f00a2adf8d0fcff8e7b184af886c3c87ec4a3f3" + +[[package]] +name = "time-macros" +version = "0.2.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f252a68540fde3a3877aeea552b832b40ab9a69e318efd078774a01ddee1ccf" +dependencies = [ + "num-conv", + "time-core", +] + +[[package]] +name = "tinyvec" +version = "1.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "445e881f4f6d382d5f27c034e25eb92edd7c784ceab92a0937db7f2e9471b938" +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.39.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9babc99b9923bfa4804bd74722ff02c0381021eafa4db9949217e3be8e84fff5" +dependencies = [ + "backtrace", + "bytes", + "libc", + "mio", + "pin-project-lite", + "socket2 0.5.7", + "tokio-macros", + "windows-sys 0.52.0", +] + +[[package]] +name = "tokio-macros" +version = "2.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "693d596312e88961bc67d7f1f97af8a70227d9f90c31bba5806eec004978d752" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.76", +] + +[[package]] +name = "tokio-postgres" +version = "0.7.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "03adcf0147e203b6032c0b2d30be1415ba03bc348901f3ff1cc0df6a733e60c3" +dependencies = [ + "async-trait", + "byteorder", + "bytes", + "fallible-iterator", + "futures-channel", + "futures-util", + "log", + "parking_lot", + "percent-encoding", + "phf", + "pin-project-lite", + "postgres-protocol", + "postgres-types", + "rand", + "socket2 0.5.7", + "tokio", + "tokio-util", + "whoami", +] + +[[package]] +name = "tokio-rustls" +version = "0.26.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c7bc40d0e5a97695bb96e27995cd3a08538541b0a846f65bba7a359f36700d4" +dependencies = [ + "rustls", + "rustls-pki-types", + "tokio", +] + +[[package]] +name = "tokio-stream" +version = "0.1.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "267ac89e0bec6e691e5813911606935d77c476ff49024f98abcea3e7b15e37af" +dependencies = [ + "futures-core", + "pin-project-lite", + "tokio", +] + +[[package]] +name = "tokio-util" +version = "0.7.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9cf6b47b3771c49ac75ad09a6162f53ad4b8088b76ac60e8ec1455b31a189fe1" +dependencies = [ + "bytes", + "futures-core", + "futures-sink", + "pin-project-lite", + "tokio", +] + +[[package]] +name = "toml_datetime" +version = "0.6.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0dd7358ecb8fc2f8d014bf86f6f638ce72ba252a2c3a2572f2a795f1d23efb41" + +[[package]] +name = "toml_edit" +version = "0.19.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b5bb770da30e5cbfde35a2d7b9b8a2c4b8ef89548a7a6aeab5c9a576e3e7421" +dependencies = [ + "indexmap 2.4.0", + "toml_datetime", + "winnow", +] + +[[package]] +name = "toml_edit" +version = "0.21.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6a8534fd7f78b5405e860340ad6575217ce99f38d4d5c8f2442cb5ecb50090e1" +dependencies = [ + "indexmap 2.4.0", + "toml_datetime", + "winnow", +] + +[[package]] +name = "tower" +version = "0.4.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8fa9be0de6cf49e536ce1851f987bd21a43b771b09473c3549a6c853db37c1c" +dependencies = [ + "futures-core", + "futures-util", + "pin-project", + "pin-project-lite", + "tokio", + "tower-layer", + "tower-service", + "tracing", +] + +[[package]] +name = "tower" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "36b837f86b25d7c0d7988f00a54e74739be6477f2aac6201b8f429a7569991b7" +dependencies = [ + "futures-core", + "futures-util", + "pin-project-lite", + "sync_wrapper 0.1.2", + "tower-layer", + "tower-service", +] + +[[package]] +name = "tower-cookies" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4fd0118512cf0b3768f7fcccf0bef1ae41d68f2b45edc1e77432b36c97c56c6d" +dependencies = [ + "async-trait", + "axum-core", + "cookie", + "futures-util", + "http", + "parking_lot", + "pin-project-lite", + "tower-layer", + "tower-service", +] + +[[package]] +name = "tower-http" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e9cd434a998747dd2c4276bc96ee2e0c7a2eadf3cae88e52be55a05fa9053f5" +dependencies = [ + "bitflags 2.6.0", + "bytes", + "http", + "http-body", + "http-body-util", + "pin-project-lite", + "tower-layer", + "tower-service", + "tracing", +] + +[[package]] +name = "tower-layer" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "121c2a6cda46980bb0fcd1647ffaf6cd3fc79a013de288782836f6df9c48780e" + +[[package]] +name = "tower-service" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8df9b6e13f2d32c91b9bd719c00d1958837bc7dec474d94952798cc8e69eeec3" + +[[package]] +name = "tower-sessions" +version = "0.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "50571505955aaa8b73f2f40489953d92b4d7ff9eb9b2a8b4e11fee0dcdb2760e" +dependencies = [ + "async-trait", + "http", + "time", + "tokio", + "tower-cookies", + "tower-layer", + "tower-service", + "tower-sessions-core", + "tower-sessions-memory-store", + "tracing", +] + +[[package]] +name = "tower-sessions-core" +version = "0.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6293bf33f1977d5ef422c2e02f909eb2c3d7bf921d93557c40d4f1b130b84aa4" +dependencies = [ + "async-trait", + "axum-core", + "base64 0.22.1", + "futures", + "http", + "parking_lot", + "rand", + "serde", + "serde_json", + "thiserror", + "time", + "tokio", + "tracing", +] + +[[package]] +name = "tower-sessions-memory-store" +version = "0.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cec5f88eeef0f036e6900217034efbce733cbdf0528a85204eaaed90bc34c354" +dependencies = [ + "async-trait", + "time", + "tokio", + "tower-sessions-core", +] + +[[package]] +name = "tracing" +version = "0.1.40" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3523ab5a71916ccf420eebdf5521fcef02141234bbc0b8a49f2fdc4544364ef" +dependencies = [ + "log", + "pin-project-lite", + "tracing-attributes", + "tracing-core", +] + +[[package]] +name = "tracing-attributes" +version = "0.1.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34704c8d6ebcbc939824180af020566b01a7c01f80641264eba0999f6c2b6be7" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.76", +] + +[[package]] +name = "tracing-core" +version = "0.1.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c06d3da6113f116aaee68e4d601191614c9053067f9ab7f6edbcb161237daa54" +dependencies = [ + "once_cell", + "valuable", +] + +[[package]] +name = "tracing-log" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee855f1f400bd0e5c02d150ae5de3840039a3f54b025156404e34c23c03f47c3" +dependencies = [ + "log", + "once_cell", + "tracing-core", +] + +[[package]] +name = "tracing-subscriber" +version = "0.3.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ad0f048c97dbd9faa9b7df56362b8ebcaa52adb06b498c050d2f4e32f90a7a8b" +dependencies = [ + "nu-ansi-term", + "sharded-slab", + "smallvec", + "thread_local", + "tracing-core", + "tracing-log", +] + +[[package]] +name = "try-lock" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" + +[[package]] +name = "typenum" +version = "1.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42ff0bf0c66b8238c6f3b578df37d0b7848e55df8577b3f74f92a69acceeb825" + +[[package]] +name = "unicode-bidi" +version = "0.3.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08f95100a766bf4f8f28f90d77e0a5461bbdb219042e7679bebe79004fed8d75" + +[[package]] +name = "unicode-ident" +version = "1.0.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b" + +[[package]] +name = "unicode-normalization" +version = "0.1.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a56d1686db2308d901306f92a263857ef59ea39678a5458e7cb17f01415101f5" +dependencies = [ + "tinyvec", +] + +[[package]] +name = "unicode-properties" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52ea75f83c0137a9b98608359a5f1af8144876eb67bcb1ce837368e906a9f524" + +[[package]] +name = "untrusted" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" + +[[package]] +name = "url" +version = "2.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "22784dbdf76fdde8af1aeda5622b546b422b6fc585325248a2bf9f5e41e94d6c" +dependencies = [ + "form_urlencoded", + "idna", + "percent-encoding", + "serde", +] + +[[package]] +name = "urlencoding" +version = "2.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "daf8dba3b7eb870caf1ddeed7bc9d2a049f3cfdfae7cb521b087cc33ae4c49da" + +[[package]] +name = "validator" +version = "0.18.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "db79c75af171630a3148bd3e6d7c4f42b6a9a014c2945bc5ed0020cbb8d9478e" +dependencies = [ + "idna", + "once_cell", + "regex", + "serde", + "serde_derive", + "serde_json", + "url", + "validator_derive", +] + +[[package]] +name = "validator_derive" +version = "0.18.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "55591299b7007f551ed1eb79a684af7672c19c3193fb9e0a31936987bb2438ec" +dependencies = [ + "darling", + "once_cell", + "proc-macro-error", + "proc-macro2", + "quote", + "syn 2.0.76", +] + +[[package]] +name = "valuable" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "830b7e5d4d90034032940e4ace0d9a9a057e7a45cd94e6c007832e39edb82f6d" + +[[package]] +name = "value-bag" +version = "1.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a84c137d37ab0142f0f2ddfe332651fdbf252e7b7dbb4e67b6c1f1b2e925101" + +[[package]] +name = "vcpkg" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" + +[[package]] +name = "version_check" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" + +[[package]] +name = "waker-fn" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "317211a0dc0ceedd78fb2ca9a44aed3d7b9b26f81870d485c07122b4350673b7" + +[[package]] +name = "want" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bfa7760aed19e106de2c7c0b581b509f2f25d3dacaf737cb82ac61bc6d760b0e" +dependencies = [ + "try-lock", +] + +[[package]] +name = "wasi" +version = "0.11.0+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" + +[[package]] +name = "wasite" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8dad83b4f25e74f184f64c43b150b91efe7647395b42289f38e50566d82855b" + +[[package]] +name = "wasm-bindgen" +version = "0.2.93" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a82edfc16a6c469f5f44dc7b571814045d60404b55a0ee849f9bcfa2e63dd9b5" +dependencies = [ + "cfg-if", + "once_cell", + "wasm-bindgen-macro", +] + +[[package]] +name = "wasm-bindgen-backend" +version = "0.2.93" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9de396da306523044d3302746f1208fa71d7532227f15e347e2d93e4145dd77b" +dependencies = [ + "bumpalo", + "log", + "once_cell", + "proc-macro2", + "quote", + "syn 2.0.76", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-futures" +version = "0.4.43" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "61e9300f63a621e96ed275155c108eb6f843b6a26d053f122ab69724559dc8ed" +dependencies = [ + "cfg-if", + "js-sys", + "wasm-bindgen", + "web-sys", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.93" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "585c4c91a46b072c92e908d99cb1dcdf95c5218eeb6f3bf1efa991ee7a68cccf" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.93" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "afc340c74d9005395cf9dd098506f7f44e38f2b4a21c6aaacf9a105ea5e1e836" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.76", + "wasm-bindgen-backend", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.93" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c62a0a307cb4a311d3a07867860911ca130c3494e8c2719593806c08bc5d0484" + +[[package]] +name = "web-sys" +version = "0.3.70" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26fdeaafd9bd129f65e7c031593c24d62186301e0c72c8978fa1678be7d532c0" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "whoami" +version = "1.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a44ab49fad634e88f55bf8f9bb3abd2f27d7204172a112c7c9987e01c1c94ea9" +dependencies = [ + "redox_syscall 0.4.1", + "wasite", + "web-sys", +] + +[[package]] +name = "winapi" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" +dependencies = [ + "winapi-i686-pc-windows-gnu", + "winapi-x86_64-pc-windows-gnu", +] + +[[package]] +name = "winapi-i686-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" + +[[package]] +name = "winapi-x86_64-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" + +[[package]] +name = "windows-core" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "33ab640c8d7e35bf8ba19b884ba838ceb4fba93a4e8c65a9059d08afcfc683d9" +dependencies = [ + "windows-targets 0.52.6", +] + +[[package]] +name = "windows-sys" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9" +dependencies = [ + "windows-targets 0.48.5", +] + +[[package]] +name = "windows-sys" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" +dependencies = [ + "windows-targets 0.52.6", +] + +[[package]] +name = "windows-sys" +version = "0.59.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" +dependencies = [ + "windows-targets 0.52.6", +] + +[[package]] +name = "windows-targets" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a2fa6e2155d7247be68c096456083145c183cbbbc2764150dda45a87197940c" +dependencies = [ + "windows_aarch64_gnullvm 0.48.5", + "windows_aarch64_msvc 0.48.5", + "windows_i686_gnu 0.48.5", + "windows_i686_msvc 0.48.5", + "windows_x86_64_gnu 0.48.5", + "windows_x86_64_gnullvm 0.48.5", + "windows_x86_64_msvc 0.48.5", +] + +[[package]] +name = "windows-targets" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" +dependencies = [ + "windows_aarch64_gnullvm 0.52.6", + "windows_aarch64_msvc 0.52.6", + "windows_i686_gnu 0.52.6", + "windows_i686_gnullvm", + "windows_i686_msvc 0.52.6", + "windows_x86_64_gnu 0.52.6", + "windows_x86_64_gnullvm 0.52.6", + "windows_x86_64_msvc 0.52.6", +] + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8" + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" + +[[package]] +name = "windows_i686_gnu" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e" + +[[package]] +name = "windows_i686_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" + +[[package]] +name = "windows_i686_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" + +[[package]] +name = "windows_i686_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406" + +[[package]] +name = "windows_i686_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" + +[[package]] +name = "winnow" +version = "0.5.40" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f593a95398737aeed53e489c785df13f3618e41dbcd6718c6addbf1395aa6876" +dependencies = [ + "memchr", +] + +[[package]] +name = "zerocopy" +version = "0.7.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b9b4fd18abc82b8136838da5d50bae7bdea537c574d8dc1a34ed098d6c166f0" +dependencies = [ + "byteorder", + "zerocopy-derive", +] + +[[package]] +name = "zerocopy-derive" +version = "0.7.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fa4f8080344d4671fb4e831a13ad1e68092748387dfc4f55e356242fae12ce3e" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.76", +] + +[[package]] +name = "zeroize" +version = "1.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ced3678a2879b30306d323f4542626697a464a97c0a07c9aebf7ebca65cd4dde" diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..ab09b8c --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,54 @@ +[package] +name = "hotel_service" +version = "0.1.0" +edition = "2021" + +[dependencies] +# API +axum = { version = "0.7.5", features = ["macros"] } +# Async +tokio = { version = "1.39.2", features = ["rt-multi-thread"] } +futures = "0.3.30" +# Auth +axum-login = "0.15.3" +tower-sessions = { version = "0.12.3", features = ["axum-core"] } +# Cryptography +sha2 = "0.10.8" +digest = "0.10.7" +rand = { version = "0.8.5", features = ["rand_chacha"] } +rand_chacha = "0.3.1" +# DB +diesel = { version = "2.2.3", features = ["postgres", "chrono", "serde_json"] } +diesel-async = { version = "0.5.0", features = ["postgres", "deadpool"] } +deadpool-diesel = { version = "0.6.1", features = ["postgres"] } +# Env +dotenvy_macro = "0.15.7" +# Error handling +thiserror = "1.0.63" +# Serialization +serde = { version = "1.0.209", features = ["derive"] } +serde_json = "1.0.127" +base64ct = { version = "1.6.0", features = ["alloc"] } +# Time +chrono = { version = "0.4.38", features = ["serde", "std"] } +# Utils +derive_more = { version = "1.0.0", features = ["from", "constructor"] } +strum = { version = "0.26.3", features = ["derive"] } +secrecy = { version = "0.8.0", features = ["serde"] } +bon = "2.0.0" + +# Validation +validator = { version = "0.18.1", features = ["derive"] } +axum-valid = { version = "0.19.0", features = ["validator"] } + +lib = { path = "../lib", features = ["axum", "serde", "derive", "diesel", "time"] } +#lib = { git = "https://github.com/emberal/rust-lib", tag = "1.4.3", features = ["axum", "serde", "derive"] } + +[dev-dependencies] +rstest = "0.22.0" +testcontainers-modules = { version = "0.9.0", features = ["postgres"] } +async-std = { version = "1.12.0", features = ["attributes"] } +tower = { version = "0.5.0", features = ["util"] } +mime = "0.3.17" +diesel_async_migrations = "0.14.0" +hyper = "1.4.1" diff --git a/Makefile.toml b/Makefile.toml new file mode 100644 index 0000000..580f542 --- /dev/null +++ b/Makefile.toml @@ -0,0 +1,27 @@ +[tasks.fmt] +command = "cargo" +args = ["fmt"] + +[tasks.lint] +command = "cargo" +args = ["clippy"] + +[tasks.release] +command = "cargo" +args = ["build", "--release"] + +[tasks.run_migrations] +command = "diesel" +args = ["migration", "run"] + +[tasks.redo_migrations] +command = "diesel" +args = ["migration", "redo"] + +[tasks.test] +command = "cargo" +args = ["test", "--all-features"] + +[tasks.coverage] +command = "cargo" +args = ["llvm-cov"] diff --git a/README.md b/README.md new file mode 100644 index 0000000..a024ba5 --- /dev/null +++ b/README.md @@ -0,0 +1,7 @@ +- [ ] Tests for handlers + - [ ] Generic AsyncConnection or mocking +- [ ] GitHub Actions +- [ ] Dockerfile +- [ ] OAuth2 ? +- [ ] OpenAPI +- [ ] Streaming diff --git a/diesel.toml b/diesel.toml new file mode 100644 index 0000000..ed486ba --- /dev/null +++ b/diesel.toml @@ -0,0 +1,9 @@ +# For documentation on how to configure this file, +# see https://diesel.rs/guides/configuring-diesel-cli + +[print_schema] +file = "src/schema.rs" +custom_type_derives = ["diesel::query_builder::QueryId", "Clone"] + +[migrations_directory] +dir = "/home/martin/git/rust/hotel_service/migrations" diff --git a/http/auth.http b/http/auth.http new file mode 100644 index 0000000..ec51e9b --- /dev/null +++ b/http/auth.http @@ -0,0 +1,18 @@ +### Register a new user +POST {{baseurl}}/auth/register +Content-Type: application/json + +{ + "email": "test@test.com", + "password": "password", + "role": "ADMIN" +} + +### Login +POST {{baseurl}}/auth/login +Content-Type: application/json + +{ + "email": "test@test.com", + "password": "password" +} \ No newline at end of file diff --git a/http/http-client.env.json b/http/http-client.env.json new file mode 100644 index 0000000..c5e384d --- /dev/null +++ b/http/http-client.env.json @@ -0,0 +1,5 @@ +{ + "dev": { + "baseurl": "http://localhost:8000" + } +} \ No newline at end of file diff --git a/http/reservation.http b/http/reservation.http new file mode 100644 index 0000000..50770e7 --- /dev/null +++ b/http/reservation.http @@ -0,0 +1,5 @@ +### List of all reservations +GET {{baseurl}}/reservation + +### GET A specific reservation +GET {{baseurl}}/reservation/1 diff --git a/migrations/.keep b/migrations/.keep new file mode 100644 index 0000000..e69de29 diff --git a/migrations/00000000000000_diesel_initial_setup/down.sql b/migrations/00000000000000_diesel_initial_setup/down.sql new file mode 100644 index 0000000..a9f5260 --- /dev/null +++ b/migrations/00000000000000_diesel_initial_setup/down.sql @@ -0,0 +1,6 @@ +-- This file was automatically created by Diesel to setup helper functions +-- and other internal bookkeeping. This file is safe to edit, any future +-- changes will be added to existing projects as new migrations. + +DROP FUNCTION IF EXISTS diesel_manage_updated_at(_tbl regclass); +DROP FUNCTION IF EXISTS diesel_set_updated_at(); diff --git a/migrations/00000000000000_diesel_initial_setup/up.sql b/migrations/00000000000000_diesel_initial_setup/up.sql new file mode 100644 index 0000000..d68895b --- /dev/null +++ b/migrations/00000000000000_diesel_initial_setup/up.sql @@ -0,0 +1,36 @@ +-- This file was automatically created by Diesel to setup helper functions +-- and other internal bookkeeping. This file is safe to edit, any future +-- changes will be added to existing projects as new migrations. + + + + +-- Sets up a trigger for the given table to automatically set a column called +-- `updated_at` whenever the row is modified (unless `updated_at` was included +-- in the modified columns) +-- +-- # Example +-- +-- ```sql +-- CREATE TABLE users (id SERIAL PRIMARY KEY, updated_at TIMESTAMP NOT NULL DEFAULT NOW()); +-- +-- SELECT diesel_manage_updated_at('users'); +-- ``` +CREATE OR REPLACE FUNCTION diesel_manage_updated_at(_tbl regclass) RETURNS VOID AS $$ +BEGIN + EXECUTE format('CREATE TRIGGER set_updated_at BEFORE UPDATE ON %s + FOR EACH ROW EXECUTE PROCEDURE diesel_set_updated_at()', _tbl); +END; +$$ LANGUAGE plpgsql; + +CREATE OR REPLACE FUNCTION diesel_set_updated_at() RETURNS trigger AS $$ +BEGIN + IF ( + NEW IS DISTINCT FROM OLD AND + NEW.updated_at IS NOT DISTINCT FROM OLD.updated_at + ) THEN + NEW.updated_at := current_timestamp; + END IF; + RETURN NEW; +END; +$$ LANGUAGE plpgsql; diff --git a/migrations/2024-08-04-214620_init_tables/down.sql b/migrations/2024-08-04-214620_init_tables/down.sql new file mode 100644 index 0000000..1df52e1 --- /dev/null +++ b/migrations/2024-08-04-214620_init_tables/down.sql @@ -0,0 +1,6 @@ +DROP TABLE IF EXISTS hotel CASCADE; +DROP TABLE IF EXISTS room CASCADE; +DROP TABLE IF EXISTS reservation CASCADE; +DROP TABLE IF EXISTS task CASCADE; +DROP TABLE IF EXISTS "user" CASCADE; +DROP TABLE IF EXISTS session CASCADE; diff --git a/migrations/2024-08-04-214620_init_tables/up.sql b/migrations/2024-08-04-214620_init_tables/up.sql new file mode 100644 index 0000000..ce04471 --- /dev/null +++ b/migrations/2024-08-04-214620_init_tables/up.sql @@ -0,0 +1,51 @@ +CREATE TABLE hotel +( + id SERIAL PRIMARY KEY, + name VARCHAR(255) NOT NULL, + address VARCHAR(255) NOT NULL +); + +CREATE TABLE room +( + id INTEGER PRIMARY KEY, + hotel_id INTEGER NOT NULL, + beds INTEGER NOT NULL CHECK (beds > 0), + size INTEGER NOT NULL CHECK (size > 0), + FOREIGN KEY (hotel_id) REFERENCES hotel (id) +); + +CREATE TABLE "user" +( + email VARCHAR(255) PRIMARY KEY, + hash VARCHAR(255) NOT NULL, + salt VARCHAR(255) NOT NULL, + role SMALLINT NOT NULL CHECK ( role IN (0, 3) ) +); + +CREATE TABLE reservation +( + id SERIAL PRIMARY KEY, + room_id INTEGER NOT NULL, + start TIMESTAMP NOT NULL, + "end" TIMESTAMP NOT NULL, + "user" VARCHAR(255) NOT NULL, + checked_in BOOLEAN NOT NULL DEFAULT FALSE, + FOREIGN KEY (room_id) REFERENCES room (id), + FOREIGN KEY ("user") REFERENCES "user" (email) +); + +CREATE TABLE task +( + id SERIAL PRIMARY KEY, + room_id INTEGER NOT NULL, + description TEXT NOT NULL, + status VARCHAR(12) NOT NULL CHECK ( status IN ('todo', 'in_progress', 'done') ), + FOREIGN KEY (room_id) REFERENCES room (id) +); + +CREATE TABLE session +( + id VARCHAR(128) PRIMARY KEY, + data JSONB NOT NULL, + expiry_date TIMESTAMP NOT NULL +) diff --git a/src/auth.rs b/src/auth.rs new file mode 100644 index 0000000..78872f8 --- /dev/null +++ b/src/auth.rs @@ -0,0 +1,102 @@ +use crate::error::ResponseError; +use crate::models::user::{User, UserRole}; +use crate::result::ResponseResult; +use crate::services::user_service::UserService; +use rand::distributions::Alphanumeric; +use rand::Rng; +use rand_chacha::rand_core::SeedableRng; +use rand_chacha::ChaCha20Rng; +use sha2::{Digest, Sha256}; +use std::future::Future; + +pub type AuthSession = axum_login::AuthSession>; + +pub async fn authenticate<'a, Ok, Fut>( + maybe_user: Option<&'a User>, + roles: impl IntoIterator, + authenticated: impl FnOnce(&'a User) -> Fut, +) -> ResponseResult +where + Fut: Future>, +{ + if let Some(user) = maybe_user { + let int_roles = roles + .into_iter() + .map(UserRole::into_i16) + .collect::>(); + + if int_roles.contains(&user.role) { + authenticated(user).await + } else { + Err(ResponseError::Forbidden) + } + } else { + Err(ResponseError::Unauthorized) + } +} + +pub(crate) fn hash_password(password: &str, salt: &[u8], output: &mut [u8; 32]) { + let hasher = Sha256::new() + .chain_update(password) + .chain_update("$") + .chain_update(salt); + output.copy_from_slice(hasher.finalize().as_slice()); +} + +pub(crate) fn generate_salt() -> Vec { + ChaCha20Rng::from_entropy() + .sample_iter(Alphanumeric) + .take(16) + .collect() +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::ok; + use crate::result::Success; + + #[tokio::test] + async fn test_authenticate_user_none() { + let user = None; + assert_eq!( + authenticate(user, [UserRole::Admin], |_| async { ok!() }).await, + Err(ResponseError::Unauthorized) + ); + } + + #[tokio::test] + async fn test_authenticate_user_unauthorized() { + let user = Some(User { + email: "test@test.com".to_string(), + role: UserRole::Guest.into_i16(), + hash: String::default(), + salt: String::default(), + }); + assert_eq!( + authenticate(user.as_ref(), [UserRole::Admin], |_| async { ok!() }).await, + Err(ResponseError::Forbidden) + ); + } + + #[tokio::test] + async fn test_authenticate_user_authorized() { + let user = Some(User { + email: "test@test.com".to_string(), + role: UserRole::Admin.into_i16(), + hash: String::default(), + salt: String::default(), + }); + assert_eq!( + authenticate(user.as_ref(), [UserRole::Admin], |_| async { ok!() }).await, + Ok(Success::Ok(())) + ); + } + + #[test] + fn test_hash_password() { + let salt = generate_salt(); + let mut hashed = [0; 32]; + hash_password("password", &salt, &mut hashed); + } +} diff --git a/src/config.rs b/src/config.rs new file mode 100644 index 0000000..0195ba0 --- /dev/null +++ b/src/config.rs @@ -0,0 +1,8 @@ +use dotenvy_macro::dotenv; + +pub(crate) const DATABASE_URL: &str = dotenv!("DATABASE_URL"); +pub(crate) const POOL_SIZE: usize = 10; +pub(crate) const SESSION_MAX_AGE: i64 = 1; +#[cfg(test)] +pub(crate) static MIGRATIONS: diesel_async_migrations::EmbeddedMigrations = + diesel_async_migrations::embed_migrations!("./migrations"); diff --git a/src/database.rs b/src/database.rs new file mode 100644 index 0000000..b559a62 --- /dev/null +++ b/src/database.rs @@ -0,0 +1,35 @@ +use crate::config; +use crate::error::AppError; +use axum::async_trait; +use deadpool_diesel::postgres::BuildError; +use deadpool_diesel::Status; +use diesel_async::pooled_connection::deadpool::{Object, Pool}; +use diesel_async::pooled_connection::AsyncDieselConnectionManager; +use diesel_async::AsyncPgConnection; + +pub type PgPool = Pool; + +#[async_trait] +pub trait GetConnection: Clone + Send + Sync { + async fn get(&self) -> Result, AppError>; + fn status(&self) -> Status; +} + +#[async_trait] +impl GetConnection for PgPool { + async fn get(&self) -> Result, AppError> { + self.get().await.map_err(Into::into) + } + fn status(&self) -> Status { + self.status() + } +} + +pub(crate) fn create_pool() -> Result { + create_pool_from_url(config::DATABASE_URL) +} + +pub(crate) fn create_pool_from_url(url: impl Into) -> Result { + let config = AsyncDieselConnectionManager::::new(url); + Pool::builder(config).max_size(config::POOL_SIZE).build() +} diff --git a/src/error.rs b/src/error.rs new file mode 100644 index 0000000..75deff2 --- /dev/null +++ b/src/error.rs @@ -0,0 +1,180 @@ +use axum::http::StatusCode; +use axum::response::{IntoResponse, Response}; +use axum_login::AuthnBackend; +use derive_more::Constructor; +use diesel::result::DatabaseErrorKind; +use diesel_async::pooled_connection::deadpool; + +use crate::services::reservation_service::ReservationError; +use lib::diesel_crud_trait::CrudError; +use lib::into_response_derive::IntoResponse; +use serde::{Deserialize, Serialize}; +use thiserror::Error; + +type DieselError = diesel::result::Error; + +#[derive(Error, Debug)] +pub enum AppError { + #[error(transparent)] + PoolError(#[from] deadpool::PoolError), + #[error(transparent)] + CrudError(CrudError), + #[error(transparent)] + DatabaseError(#[from] diesel::result::Error), + #[error("Auth session error: {0}")] + AuthSessionError(String), + #[error("Bad request: {0}")] + BadRequest(String), + #[error("Resource not found")] + NotFound, + #[error("Unauthorized. Please log in at /auth/login")] + Unauthorized, + #[error("Forbidden. Insufficient permissions")] + Forbidden, + #[error("Internal server error: {0}")] + InternalServerError(String), + #[error(transparent)] + JsonError(#[from] serde_json::Error), + #[error(transparent)] + TimeParseError(#[from] chrono::ParseError), + #[error(transparent)] + IdParseError(#[from] std::num::ParseIntError), + #[error(transparent)] + FromUtf8Error(#[from] std::string::FromUtf8Error), + #[error("Base64 error: {0}")] + Base64Error(String), +} + +#[derive(Debug, Error, PartialEq)] +pub enum ResponseError { + #[error("{0}")] + BadRequest(String), // 400 + #[error("User is not authenticated. Please log in at /auth/login")] + Unauthorized, // 401 + #[error("Forbidden. Insufficient permissions")] + Forbidden, // 403 + #[error("{0}")] + NotFound(String), // 404 + #[error("{0}")] + Conflict(String), // 409 + #[error("{0}")] + InternalServerError(String), // 500 +} + +#[derive(Debug, Serialize, Deserialize, IntoResponse, Constructor)] +pub(crate) struct ErrorResponseBody { + pub kind: i32, + pub error: String, +} + +impl IntoResponse for ResponseError { + fn into_response(self) -> Response { + match self { + ResponseError::BadRequest(error) => { + (StatusCode::BAD_REQUEST, ErrorResponseBody::new(400, error)) + } + ResponseError::Unauthorized => ( + StatusCode::UNAUTHORIZED, + ErrorResponseBody::new(401, self.to_string()), + ), + ResponseError::Forbidden => ( + StatusCode::FORBIDDEN, + ErrorResponseBody::new(403, self.to_string()), + ), + ResponseError::NotFound(error) => { + (StatusCode::NOT_FOUND, ErrorResponseBody::new(404, error)) + } + ResponseError::Conflict(error) => { + (StatusCode::CONFLICT, ErrorResponseBody::new(409, error)) + } + ResponseError::InternalServerError(error) => ( + StatusCode::INTERNAL_SERVER_ERROR, + ErrorResponseBody::new(500, error), + ), + } + .into_response() + } +} + +impl From for ResponseError { + fn from(value: CrudError) -> Self { + match &value { + CrudError::NotFound => Self::NotFound(value.to_string()), + CrudError::Other(diesel::result::Error::DatabaseError( + DatabaseErrorKind::UniqueViolation, + error, + )) => Self::Conflict(error.message().to_string()), + _ => Self::InternalServerError(value.to_string()), + } + } +} + +impl From for ResponseError { + fn from(value: ReservationError) -> Self { + match value { + ReservationError::NotFound => Self::NotFound(value.to_string()), + ReservationError::Other(error) => Self::InternalServerError(error), + _ => Self::BadRequest(value.to_string()), + } + } +} + +impl From for ResponseError { + fn from(value: deadpool::PoolError) -> Self { + Self::InternalServerError(value.to_string()) + } +} + +impl From> for ResponseError { + fn from(value: axum_login::Error) -> Self { + Self::BadRequest(value.to_string()) + } +} + +impl From for ResponseError { + fn from(value: AppError) -> Self { + match value { + AppError::NotFound => Self::NotFound(value.to_string()), + AppError::BadRequest(error) => Self::BadRequest(error), + AppError::Unauthorized => Self::Unauthorized, + AppError::Forbidden => Self::Forbidden, + AppError::CrudError(error) => error.into(), + _ => Self::InternalServerError(value.to_string()), + } + } +} + +impl IntoResponse for AppError { + fn into_response(self) -> Response { + match self { + Self::NotFound => (StatusCode::NOT_FOUND, self.to_string()).into_response(), + Self::BadRequest(_) => (StatusCode::BAD_REQUEST, self.to_string()).into_response(), + Self::Unauthorized => (StatusCode::UNAUTHORIZED, self.to_string()).into_response(), + Self::Forbidden => (StatusCode::FORBIDDEN, self.to_string()).into_response(), + otherwise => (StatusCode::INTERNAL_SERVER_ERROR, otherwise.to_string()).into_response(), + } + } +} + +impl From for AppError { + fn from(value: base64ct::Error) -> Self { + Self::Base64Error(value.to_string()) + } +} + +impl From for AppError { + fn from(value: CrudError) -> Self { + match value { + CrudError::NotFound => Self::NotFound, + CrudError::Other(DieselError::DatabaseError( + DatabaseErrorKind::ForeignKeyViolation, + error, + )) => Self::BadRequest(format!( + "Foreign key violation: '{}' on constraint '{}'", + error.message(), + error.constraint_name().unwrap_or_default() + )), + otherwise => Self::CrudError(otherwise), + } + } +} diff --git a/src/main.rs b/src/main.rs new file mode 100644 index 0000000..4976de5 --- /dev/null +++ b/src/main.rs @@ -0,0 +1,100 @@ +#[cfg(test)] +#[macro_use] +extern crate rstest; + +use crate::database::{create_pool, GetConnection}; +use crate::services::session_service::SessionService; +use crate::services::user_service::UserService; +use axum::Router; +use axum_login::tower_sessions::SessionManagerLayer; +use axum_login::AuthManagerLayerBuilder; +use lib::axum::app::AppBuilder; +use tower_sessions::cookie::time::Duration; +use tower_sessions::Expiry; + +mod auth; +mod config; +mod database; +mod error; +mod models; +mod result; +mod routes; +mod schema; +mod services; +#[cfg(test)] +mod test; + +pub fn create_app(pool: Pool) -> Router +where + Pool: GetConnection + 'static, +{ + let session_service = SessionService::new(pool.clone()); + let user_service = UserService::new(pool.clone()); + + let session_layer = SessionManagerLayer::new(session_service) + .with_secure(false) + .with_expiry(Expiry::OnInactivity(Duration::days( + config::SESSION_MAX_AGE, + ))); + let auth_layer = AuthManagerLayerBuilder::new(user_service.clone(), session_layer).build(); + AppBuilder::new() + .routes([ + routes::hotel::router() + .with_state(pool.clone()) + .login_required::(), + routes::reservation::router() + .with_state(pool.clone()) + .login_required::(), + routes::room::router() + .with_state(pool.clone()) + .login_required::(), + routes::task::router() + .with_state(pool.clone()) + .login_required::(), + routes::user::router() + .with_state(pool) + .login_required::(), + routes::auth::router().with_state(user_service), + ]) + .layer(auth_layer) + .build() +} + +#[tokio::main] +async fn main() { + let pool = create_pool().unwrap(); + AppBuilder::from_router(create_app(pool)) + .serve() + .await + .unwrap(); +} + +trait LoginRequired { + fn login_required(self) -> Self + where + Pool: GetConnection + Send + Sync + 'static; +} + +impl LoginRequired for Router +where + S: Clone + Send + Sync + 'static, +{ + fn login_required(self) -> Self + where + Pool: GetConnection + Send + Sync + 'static, + { + use axum_login::axum::{ + middleware::{from_fn, Next}, + response::IntoResponse, + }; + self.route_layer(from_fn( + |auth_session: axum_login::AuthSession>, req, next: Next| async move { + if auth_session.user.is_some() { + next.run(req).await + } else { + axum::http::StatusCode::UNAUTHORIZED.into_response() + } + }, + )) + } +} diff --git a/src/models/common.rs b/src/models/common.rs new file mode 100644 index 0000000..46201c2 --- /dev/null +++ b/src/models/common.rs @@ -0,0 +1,34 @@ +use lib::axum::wrappers::Count; +use lib::diesel_crud_trait::CrudError; + +pub(crate) fn map_count(count: Result) -> Result { + match count { + Ok(0) => Err(CrudError::NotFound), + Ok(n) => Ok(n.into()), + Err(err) => Err(err), + } +} + +#[cfg(test)] +mod tests { + use super::*; + use lib::diesel_crud_trait::CrudError; + + #[test] + fn test_map_count_0_is_not_found() { + assert_eq!(map_count(Ok(0)), Err(CrudError::NotFound)); + } + + #[test] + fn test_map_count_n_is_count() { + assert_eq!(map_count(Ok(1)), Ok(1.into())); + } + + #[test] + fn test_map_count_error_is_error() { + assert_eq!( + map_count(Err(CrudError::PoolError("error".to_string()))), + Err(CrudError::PoolError("error".to_string())) + ); + } +} diff --git a/src/models/hotel.rs b/src/models/hotel.rs new file mode 100644 index 0000000..809072b --- /dev/null +++ b/src/models/hotel.rs @@ -0,0 +1,33 @@ +use derive_more::Constructor; +use diesel::{AsChangeset, Identifiable, Insertable, Queryable, Selectable}; +use lib::diesel_crud_derive::DieselCrud; +use lib::into_response_derive::IntoResponse; +use serde::{Deserialize, Serialize}; +use validator::Validate; + +#[derive( + Queryable, Selectable, Identifiable, AsChangeset, Serialize, IntoResponse, DieselCrud, Validate, +)] +#[diesel_crud(insert = CreateHotel)] +#[diesel(table_name = crate::schema::hotel)] +#[diesel(check_for_backend(diesel::pg::Pg))] +pub struct Hotel { + #[diesel_crud(pk)] + pub id: i32, + #[validate(length(min = 1))] + pub name: String, + #[validate(length(min = 1))] + pub address: String, +} + +#[derive(Insertable, Deserialize, Validate, Constructor)] +#[diesel(table_name = crate::schema::hotel)] +#[diesel(check_for_backend(diesel::pg::Pg))] +pub struct CreateHotel { + #[validate(range(min = 1))] + pub id: Option, + #[validate(length(min = 1))] + pub name: String, + #[validate(length(min = 1))] + pub address: String, +} diff --git a/src/models/mod.rs b/src/models/mod.rs new file mode 100644 index 0000000..c262407 --- /dev/null +++ b/src/models/mod.rs @@ -0,0 +1,7 @@ +pub mod common; +pub mod hotel; +pub mod reservation; +pub mod room; +pub mod session; +pub mod task; +pub mod user; diff --git a/src/models/reservation.rs b/src/models/reservation.rs new file mode 100644 index 0000000..42a192f --- /dev/null +++ b/src/models/reservation.rs @@ -0,0 +1,109 @@ +use chrono::{NaiveDateTime, Utc}; +use derive_more::Constructor; +use diesel::{AsChangeset, Insertable, Queryable, Selectable}; +use lib::diesel_crud_derive::DieselCrud; +use lib::into_response_derive::IntoResponse; +use serde::{Deserialize, Serialize}; +use std::borrow::Cow; +use validator::{Validate, ValidationError}; + +#[derive(Clone, Validate, Insertable, Deserialize, Constructor)] +#[validate(schema(function = "validate_create_reservation"))] +#[diesel(table_name = crate::schema::reservation)] +pub struct CreateReservation { + pub room_id: i32, + pub start: NaiveDateTime, + pub end: NaiveDateTime, + #[validate(email)] + pub user: String, +} + +fn validate_create_reservation(reservation: &CreateReservation) -> Result<(), ValidationError> { + _validate_reservation(&reservation.start, &reservation.end) +} + +#[derive( + Clone, + Validate, + Queryable, + Selectable, + AsChangeset, + Serialize, + Deserialize, + IntoResponse, + DieselCrud, +)] +#[diesel_crud(insert = CreateReservation)] +#[validate(schema(function = "validate_reservation"))] +#[diesel(table_name = crate::schema::reservation)] +#[diesel(check_for_backend(diesel::pg::Pg))] +pub struct Reservation { + #[diesel_crud(pk)] + pub id: i32, + pub room_id: i32, + pub start: NaiveDateTime, + pub end: NaiveDateTime, + #[validate(email)] + pub user: String, + pub checked_in: bool, +} + +fn validate_reservation(reservation: &Reservation) -> Result<(), ValidationError> { + _validate_reservation(&reservation.start, &reservation.end) +} + +fn _validate_reservation( + start: &NaiveDateTime, + end: &NaiveDateTime, +) -> Result<(), ValidationError> { + if start >= end { + let mut err = ValidationError::new("start must be before end"); + err.add_param(Cow::from("start"), start); + err.add_param(Cow::from("end"), end); + Err(err) + } else if start < &Utc::now().naive_utc() { + let mut err = ValidationError::new("start must be in the future"); + err.add_param(Cow::from("start"), start); + Err(err) + } else { + Ok(()) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_validate_reservation_ok() { + let reservation = CreateReservation { + room_id: 1, + start: Utc::now().naive_utc() + chrono::Duration::days(1), + end: Utc::now().naive_utc() + chrono::Duration::days(2), + user: "a@example.com".to_string(), + }; + assert!(validate_create_reservation(&reservation).is_ok()); + } + + #[test] + fn test_validate_reservation_start_after_end() { + let reservation = CreateReservation { + room_id: 1, + start: Utc::now().naive_utc() + chrono::Duration::days(1), + end: Utc::now().naive_utc(), + user: "a@example.com".to_string(), + }; + assert!(validate_create_reservation(&reservation).is_err()); + } + + #[test] + fn test_validate_reservation_start_in_past() { + let reservation = CreateReservation { + room_id: 1, + start: Utc::now().naive_utc() - chrono::Duration::days(1), + end: Utc::now().naive_utc(), + user: "a@example.com".to_string(), + }; + assert!(validate_create_reservation(&reservation).is_err()); + } +} diff --git a/src/models/room.rs b/src/models/room.rs new file mode 100644 index 0000000..90b71df --- /dev/null +++ b/src/models/room.rs @@ -0,0 +1,34 @@ +use diesel::{AsChangeset, Associations, Identifiable, Insertable, Queryable, Selectable}; +use lib::diesel_crud_derive::DieselCrud; +use lib::into_response_derive::IntoResponse; +use serde::{Deserialize, Serialize}; +use validator::Validate; + +#[derive( + Debug, + PartialEq, + Queryable, + Selectable, + Identifiable, + Associations, + AsChangeset, + Insertable, + Serialize, + Deserialize, + IntoResponse, + Validate, + DieselCrud, +)] +#[diesel(belongs_to(crate::models::hotel::Hotel))] +#[diesel(table_name = crate::schema::room)] +#[diesel(check_for_backend(diesel::pg::Pg))] +pub struct Room { + #[diesel_crud(pk)] + pub id: i32, + #[validate(range(min = 1))] + pub hotel_id: i32, + #[validate(range(min = 1))] + pub beds: i32, + #[validate(range(min = 1))] + pub size: i32, +} diff --git a/src/models/session.rs b/src/models/session.rs new file mode 100644 index 0000000..abe4135 --- /dev/null +++ b/src/models/session.rs @@ -0,0 +1,75 @@ +use crate::error::AppError; +use chrono::{DateTime, NaiveDateTime}; +use diesel::{Insertable, Queryable, Selectable}; +use lib::diesel_crud_derive::{DieselCrudCreate, DieselCrudDelete, DieselCrudRead}; +use serde_json::Value; +use std::collections::HashMap; +use std::time::SystemTime; +use tower_sessions::cookie::time::OffsetDateTime; +use tower_sessions::session; +use tower_sessions::session::Record; + +#[derive(Insertable, Queryable, Selectable, DieselCrudCreate, DieselCrudRead, DieselCrudDelete)] +#[diesel(table_name = crate::schema::session)] +#[diesel(check_for_backend(diesel::pg::Pg))] +pub struct Session { + #[diesel_crud(pk)] + pub id: String, + pub data: Value, + pub expiry_date: NaiveDateTime, +} + +impl TryFrom for Session { + type Error = AppError; + fn try_from(value: Record) -> Result { + Ok(Self { + id: value.id.0.to_string(), + data: serde_json::to_value(value.data)?, + expiry_date: DateTime::from_timestamp_micros(value.expiry_date.unix_timestamp()) + .ok_or(AppError::InternalServerError( + "Failed to convert time".into(), + ))? + .naive_utc(), + }) + } +} + +impl Session { + pub fn try_into_record(self) -> Result { + Ok(Record { + id: session::Id(self.id.trim().parse::()?), + expiry_date: self.expiry_date.into_offset_date_time(), + data: self.data.into_hash_map(), + }) + } +} + +trait IntoOffsetDateTime { + fn into_offset_date_time(self) -> OffsetDateTime; +} + +impl IntoOffsetDateTime for NaiveDateTime { + fn into_offset_date_time(self) -> OffsetDateTime { + let system_time = SystemTime::from(self.and_utc()); + OffsetDateTime::from(system_time) + } +} + +impl IntoHashMap for Value { + fn into_hash_map(self) -> HashMap { + if let Value::Object(map) = self { + map.keys().cloned().zip(map.values().cloned()).collect() + } else { + HashMap::new() + } + } +} + +trait IntoHashMap { + fn into_hash_map(self) -> HashMap; +} + +#[cfg(test)] +mod tests { + // TODO +} diff --git a/src/models/task.rs b/src/models/task.rs new file mode 100644 index 0000000..912ef26 --- /dev/null +++ b/src/models/task.rs @@ -0,0 +1,78 @@ +use diesel::{AsChangeset, Insertable, Queryable, Selectable}; +use lib::diesel_crud_derive::DieselCrud; +use lib::into_response_derive::IntoResponse; +use serde::{Deserialize, Serialize}; +use std::borrow::Cow; +use strum::Display; +use validator::{Validate, ValidationError}; + +#[derive( + Queryable, Selectable, AsChangeset, Validate, Serialize, Deserialize, IntoResponse, DieselCrud, +)] +#[diesel_crud(insert = CreateTask)] +#[diesel(table_name = crate::schema::task)] +#[diesel(check_for_backend(diesel::pg::Pg))] +pub struct Task { + #[diesel_crud(pk)] + pub id: i32, + #[validate(range(min = 1))] + pub room_id: i32, + #[validate(length(min = 1))] + pub description: String, + #[validate(custom(function = "validate_task_status"))] + pub status: String, // TODO use Enum +} + +#[derive(Insertable, Validate, Deserialize)] +#[diesel(table_name = crate::schema::task)] +#[diesel(check_for_backend(diesel::pg::Pg))] +pub struct CreateTask { + #[validate(range(min = 1))] + pub room_id: i32, + #[validate(length(min = 1))] + pub description: String, + #[validate(custom(function = "validate_task_status"))] + pub status: String, +} + +fn validate_task_status(status: &str) -> Result<(), ValidationError> { + if ["todo", "in_progress", "done"].contains(&status) { + Ok(()) + } else { + let mut err = ValidationError::new("status must be 'todo', 'in_progress' or 'done'"); + err.add_param(Cow::from("status"), &status); + Err(err) + } +} + +#[derive(Debug, Clone, Copy, Display, Deserialize)] +#[strum(serialize_all = "snake_case")] +pub enum TaskStatus { + Todo, + InProgress, + Done, +} + +#[cfg(test)] +mod tests { + use super::*; + + #[rstest] + fn test_validate_task_status_legal_status( + #[values("todo", "in_progress", "done")] status: &str, + ) { + assert!(validate_task_status(status).is_ok()); + } + + #[rstest] + fn test_validate_task_status_illegal_status(#[values("illegal", "status")] status: &str) { + assert!(validate_task_status(status).is_err()); + } + + #[test] + fn test_enum_display() { + assert_eq!(TaskStatus::Todo.to_string(), "todo"); + assert_eq!(TaskStatus::InProgress.to_string(), "in_progress"); + assert_eq!(TaskStatus::Done.to_string(), "done"); + } +} diff --git a/src/models/user.rs b/src/models/user.rs new file mode 100644 index 0000000..2643e71 --- /dev/null +++ b/src/models/user.rs @@ -0,0 +1,126 @@ +use crate::auth::{generate_salt, hash_password}; +use crate::error::AppError; +use axum_login::AuthUser; +use base64ct::{Base64, Encoding}; +use derive_more::From; +use diesel::{Insertable, Queryable, Selectable}; +use lib::diesel_crud_derive::{DieselCrudCreate, DieselCrudDelete, DieselCrudList, DieselCrudRead}; +use lib::into_response_derive::IntoResponse; +use secrecy::{ExposeSecret, SecretString}; +use serde::{Deserialize, Serialize}; +use strum::EnumIs; +use validator::Validate; + +#[derive( + Debug, + Clone, + Queryable, + Selectable, + Insertable, + Validate, + Serialize, + Deserialize, + IntoResponse, + DieselCrudRead, + DieselCrudCreate, + DieselCrudDelete, + DieselCrudList, +)] +#[diesel(table_name = crate::schema::user)] +#[diesel(check_for_backend(diesel::pg::Pg))] +pub struct User { + #[diesel_crud(pk)] + #[validate(email)] + pub email: String, + pub hash: String, + pub salt: String, + pub role: i16, // TODO: use enum +} + +#[derive(Validate, Deserialize)] +pub struct UserCredentials { + #[validate(email)] + pub email: String, + pub password: SecretString, +} + +#[derive(Clone, Validate, Deserialize)] +pub struct CreateUser { + #[validate(email)] + pub email: String, + pub password: SecretString, + pub role: Option, +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize, EnumIs)] +#[serde(rename_all = "SCREAMING_SNAKE_CASE")] +pub enum UserRole { + Admin, + Receptionist, + Janitor, + #[default] + Guest, +} + +impl CreateUser { + pub fn new(email: impl Into, password: impl Into) -> Self { + Self { + email: email.into(), + password: SecretString::new(password.into()), + role: None, + } + } +} + +impl User { + pub fn hash_from_credentials(create_user: CreateUser) -> Result { + let salt = generate_salt(); + let mut hashed = [0; 32]; + hash_password(create_user.password.expose_secret(), &salt, &mut hashed); + + Ok(Self { + email: create_user.email.clone(), + hash: Base64::encode_string(&hashed), + salt: String::from_utf8(salt.to_vec())?, + role: create_user.role.unwrap_or_default() as i16, + }) + } +} + +impl UserRole { + pub fn into_i16(self) -> i16 { + self as i16 + } +} + +impl AuthUser for User { + type Id = String; + + fn id(&self) -> Self::Id { + self.email.clone() + } + + fn session_auth_hash(&self) -> &[u8] { + // TODO use decode? + self.hash.as_bytes() + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_hash_from_credentials() { + let credentials = CreateUser { + email: "me@test.com".to_string(), + password: SecretString::new("password".to_string()), + role: None, + }; + let user = User::hash_from_credentials(credentials.clone()).unwrap(); + assert_eq!(user.email, credentials.email); + assert_eq!(user.role, UserRole::Guest.into_i16()); + assert!(!user.hash.is_empty()); + assert!(!user.salt.is_empty()); + } +} diff --git a/src/result.rs b/src/result.rs new file mode 100644 index 0000000..dcf43a9 --- /dev/null +++ b/src/result.rs @@ -0,0 +1,85 @@ +use crate::error::ResponseError; +use axum::http::StatusCode; +use axum::response::{IntoResponse, Response}; + +pub type ResponseResult = Result, ResponseError>; + +#[derive(Debug, Clone, PartialEq)] +pub enum Success { + Ok(T), + Created(T), +} + +impl IntoResponse for Success { + fn into_response(self) -> Response { + match self { + Success::Ok(value) => (StatusCode::OK, value).into_response(), + Success::Created(value) => (StatusCode::CREATED, value).into_response(), + } + } +} + +impl From for Success { + fn from(value: T) -> Self { + Success::Ok(value) + } +} + +#[macro_export] +macro_rules! ok { + () => { + Ok($crate::result::Success::Ok(())) + }; + ($expr:expr) => { + Ok($crate::result::Success::Ok($expr)) + }; +} + +#[macro_export] +macro_rules! created { + () => { + Ok($crate::result::Success::Created(())) + }; + ($expr:expr) => { + Ok($crate::result::Success::Created($expr)) + }; +} + +#[macro_export] +macro_rules! bad_request { + ($expr:expr) => { + Err($crate::error::ResponseError::BadRequest($expr.to_string())) + }; +} + +#[macro_export] +macro_rules! not_found { + ($expr:expr) => { + Err($crate::error::ResponseError::NotFound($expr.to_string())) + }; +} + +#[macro_export] +macro_rules! unauthorized { + ($expr:expr) => { + Err($crate::error::ResponseError::Unauthorized( + $expr.to_string(), + )) + }; +} + +#[macro_export] +macro_rules! forbidden { + () => { + Err($crate::error::ResponseError::Forbidden) + }; +} + +#[macro_export] +macro_rules! internal_server_error { + ($expr:expr) => { + Err($crate::error::ResponseError::InternalServerError( + $expr.to_string(), + )) + }; +} diff --git a/src/routes/auth.rs b/src/routes/auth.rs new file mode 100644 index 0000000..72cef36 --- /dev/null +++ b/src/routes/auth.rs @@ -0,0 +1,139 @@ +use crate::auth::AuthSession; +use crate::database::GetConnection; +use crate::models::user::{CreateUser, User, UserCredentials}; +use crate::result::{ResponseResult, Success}; +use crate::services::user_service::UserService; +use crate::{bad_request, internal_server_error, ok}; +use axum::extract::State; +use axum::Json; +use axum_valid::Valid; +// router!( +// "/auth", +// routes!( +// post "/login" => login::, +// post "/register" => register:: +// ), +// Pool: Clone, Send, Sync, GetConnection -> UserService +// ); + +pub fn router( +) -> axum::Router> { + axum::Router::new().nest( + "/auth", + axum::Router::new() + .route("/login", axum::routing::post(login::)) + .route("/register", axum::routing::post(register::)), + ) +} + +async fn login( + mut auth_session: AuthSession, + Valid(Json(credentials)): Valid>, +) -> ResponseResult +where + Pool: GetConnection, +{ + let user = match auth_session.authenticate(credentials).await { + Ok(Some(user)) => user, + Ok(None) => return bad_request!("Invalid credentials"), + Err(error) => return internal_server_error!(error), + }; + auth_session.login(&user).await?; + ok!() +} + +async fn register( + State(service): State>, + Valid(Json(create_user)): Valid>, +) -> ResponseResult +where + Pool: GetConnection, +{ + service + .insert(create_user) + .await + .map(Success::Created) + .map_err(Into::into) +} + +#[cfg(test)] +mod tests { + use crate::create_app; + use crate::error::ErrorResponseBody; + use crate::models::user::{CreateUser, User, UserRole}; + use crate::test::{create_test_containers_pool, create_test_pool, BuildJson, DeserializeInto}; + use axum::http::request::Builder; + use axum::http::{Request, StatusCode}; + use secrecy::ExposeSecret; + use serde::ser::SerializeStruct; + use serde::{Serialize, Serializer}; + use tower::ServiceExt; + + fn register() -> Builder { + Request::builder().uri("/auth/register").method("POST") + } + + #[tokio::test] + async fn test_register_created() { + let pool = create_test_pool().await.unwrap(); + let app = create_app(pool); + let create_user = CreateUser::new("test@email.com", "password"); + let create_email = create_user.email.clone(); + + let response = app + .oneshot(register().json(create_user).unwrap()) + .await + .unwrap(); + + let status = response.status(); + let user: User = response.deserialize_into().await.unwrap(); + + assert_eq!(status, StatusCode::CREATED); + assert_eq!(user.email, create_email); + assert_eq!(user.role, UserRole::default() as i16); + assert!(!user.hash.is_empty()); + assert!(!user.salt.is_empty()); + } + + #[tokio::test] + async fn test_register_email_already_registered() { + let test_container = create_test_containers_pool().await.unwrap(); + let app = create_app(test_container.pool); + + let create_user = CreateUser::new("test@email.com", "password"); + + let call = || async { + app.clone() + .oneshot(register().json(create_user.clone()).unwrap()) + .await + .unwrap() + }; + let created = call().await; + let conflicted = call().await; + + let conflicted_status = conflicted.status(); + + let response: ErrorResponseBody = conflicted.deserialize_into().await.unwrap(); + + assert_eq!(created.status(), StatusCode::CREATED); + assert_eq!(conflicted_status, StatusCode::CONFLICT); + assert!( + response.error.contains("user_pkey"), + "Error: {}", + response.error + ); + } + + impl Serialize for CreateUser { + fn serialize(&self, serializer: S) -> Result + where + S: Serializer, + { + let mut s = serializer.serialize_struct("CreateUser", 2)?; + s.serialize_field("email", &self.email)?; + s.serialize_field("password", &self.password.expose_secret())?; + s.serialize_field("role", &self.role)?; + s.end() + } + } +} diff --git a/src/routes/hotel.rs b/src/routes/hotel.rs new file mode 100644 index 0000000..7bdceef --- /dev/null +++ b/src/routes/hotel.rs @@ -0,0 +1,54 @@ +use axum::extract::{Path, State}; + +use lib::diesel_crud_trait::{DieselCrudList, DieselCrudRead}; + +use crate::auth::{authenticate, AuthSession}; +use crate::models::hotel::Hotel; +use crate::models::user::UserRole; +use crate::result::{ResponseResult, Success}; +use crate::GetConnection; +use lib::axum::wrappers::Array; +use lib::{router, routes}; + +router!( + "/hotel", + routes! { + get "/:id" => get_hotel::, + get "/list" => list_hotels::, + }, + T: Clone, GetConnection +); + +async fn get_hotel( + auth_session: AuthSession, + State(pool): State, + Path(id): Path, +) -> ResponseResult +where + Pool: GetConnection, +{ + authenticate(auth_session.user.as_ref(), [UserRole::Admin], |_| async { + Hotel::read(id, pool.get().await?.as_mut()) + .await + .map(Success::Ok) + .map_err(Into::into) + }) + .await +} + +async fn list_hotels( + auth_session: AuthSession, + State(pool): State, +) -> ResponseResult> +where + Pool: GetConnection, +{ + authenticate(auth_session.user.as_ref(), [UserRole::Admin], |_| async { + Hotel::list(pool.get().await?.as_mut()) + .await + .map(Array::from) + .map(Success::Ok) + .map_err(Into::into) + }) + .await +} diff --git a/src/routes/mod.rs b/src/routes/mod.rs new file mode 100644 index 0000000..ef262ea --- /dev/null +++ b/src/routes/mod.rs @@ -0,0 +1,6 @@ +pub mod auth; +pub mod hotel; +pub mod reservation; +pub mod room; +pub mod task; +pub mod user; diff --git a/src/routes/reservation.rs b/src/routes/reservation.rs new file mode 100644 index 0000000..db77c87 --- /dev/null +++ b/src/routes/reservation.rs @@ -0,0 +1,191 @@ +use crate::auth::{authenticate, AuthSession}; +use crate::error::ResponseError; +use crate::models::reservation::{CreateReservation, Reservation}; +use crate::models::user::UserRole; +use crate::result::{ResponseResult, Success}; +use crate::{forbidden, ok, GetConnection}; +use axum::extract::{Path, State}; +use axum::Json; +use axum_valid::Valid; +use lib::axum::wrappers::Array; +use lib::diesel_crud_trait::{ + DieselCrudCreate, DieselCrudDelete, DieselCrudList, DieselCrudRead, DieselCrudUpdate, +}; +use lib::time::DateTimeInterval; +use lib::{router, routes}; + +router!( + "/reservation", + routes! { + get "/:id" => get_reservation::, + get "/" => list_reservations::, + post "/" => create_reservation::, + patch "/" => update_reservation::, + delete "/:id" => delete_reservation::, + patch "/:id/check-in" => check_in::, + patch "/:id/check-out" => check_out::, + }, + T: Clone, GetConnection +); + +async fn get_reservation( + auth_session: AuthSession, + State(pool): State, + Path(id): Path, +) -> ResponseResult +where + Pool: GetConnection, +{ + authenticate( + auth_session.user.as_ref(), + [UserRole::Admin, UserRole::Receptionist, UserRole::Guest], + |user| async { + let result = Reservation::read(id, pool.get().await?.as_mut()).await?; + if user.role == UserRole::Admin as i16 + || user.role == UserRole::Receptionist as i16 + || result.user == user.email + { + ok!(result) + } else { + forbidden!() + } + }, + ) + .await +} + +async fn list_reservations( + auth_session: AuthSession, + State(pool): State, +) -> ResponseResult> +where + Pool: GetConnection, +{ + authenticate( + auth_session.user.as_ref(), + [UserRole::Admin, UserRole::Receptionist], + |_| async { + Reservation::list(pool.get().await?.as_mut()) + .await + .map(Array::from) + .map(Success::Ok) + .map_err(Into::into) + }, + ) + .await +} + +async fn create_reservation( + auth_session: AuthSession, + State(pool): State, + Valid(Json(reservation)): Valid>, +) -> ResponseResult +where + Pool: GetConnection, +{ + authenticate( + auth_session.user.as_ref(), + [UserRole::Admin, UserRole::Receptionist, UserRole::Guest], + |_| async { + let interval = DateTimeInterval::new_safe(reservation.start, reservation.end).ok_or( + ResponseError::BadRequest("Start date must be before end date".into()), + )?; + let mut conn = pool.get().await?; + Reservation::room_available(reservation.room_id, interval, &mut conn).await?; + Reservation::insert(reservation, &mut conn) + .await + .map(Success::Created) + .map_err(Into::into) + }, + ) + .await +} + +async fn update_reservation( + auth_session: AuthSession, + State(pool): State, + Valid(Json(reservation)): Valid>, +) -> ResponseResult +where + Pool: GetConnection, +{ + authenticate( + auth_session.user.as_ref(), + [UserRole::Admin, UserRole::Receptionist], + |_| async { + let interval = DateTimeInterval::new_safe(reservation.start, reservation.end).ok_or( + ResponseError::BadRequest("Start date must be before end date".into()), + )?; + let mut conn = pool.get().await?; + Reservation::room_available(reservation.room_id, interval, &mut conn).await?; + Reservation::update(reservation, &mut conn) + .await + .map(|_| Success::Ok(())) + .map_err(Into::into) + }, + ) + .await +} + +async fn delete_reservation( + auth_session: AuthSession, + State(pool): State, + Path(id): Path, +) -> ResponseResult +where + Pool: GetConnection, +{ + authenticate( + auth_session.user.as_ref(), + [UserRole::Admin, UserRole::Receptionist], + |_| async { + Reservation::delete(id, pool.get().await?.as_mut()) + .await + .map(|_| Success::Ok(())) + .map_err(Into::into) + }, + ) + .await +} + +async fn check_in( + auth_session: AuthSession, + State(pool): State, + Path(id): Path, +) -> ResponseResult +where + Pool: GetConnection, +{ + authenticate( + auth_session.user.as_ref(), + [UserRole::Admin, UserRole::Receptionist], + |_| async { + Reservation::check_in(id, pool.get().await?.as_mut()) + .await + .map(Success::Ok) + .map_err(Into::into) + }, + ) + .await +} + +async fn check_out( + auth_session: AuthSession, + State(pool): State, + Path(id): Path, +) -> ResponseResult +where + Pool: GetConnection, +{ + authenticate( + auth_session.user.as_ref(), + [UserRole::Admin, UserRole::Receptionist], + |_| async { + Reservation::check_out(id, pool.get().await?.as_mut()) + .await + .map(Success::Ok) + .map_err(Into::into) + }, + ) + .await +} diff --git a/src/routes/room.rs b/src/routes/room.rs new file mode 100644 index 0000000..05c2cad --- /dev/null +++ b/src/routes/room.rs @@ -0,0 +1,150 @@ +use crate::auth::{authenticate, AuthSession}; +use crate::models::room::Room; +use crate::models::user::UserRole; +use crate::result::{ResponseResult, Success}; +use crate::services::room_service::RoomQuery; +use crate::{ok, GetConnection}; +use axum::extract::{Path, State}; +use axum::Json; +use axum_valid::Valid; +use lib::axum::wrappers::Array; +use lib::diesel_crud_trait::{ + DieselCrudCreate, DieselCrudDelete, DieselCrudList, DieselCrudRead, DieselCrudUpdate, +}; +use lib::{router, routes}; + +router!( + "/room", + routes!( + get "/:id" => get_room::, + post "/" => create_room::, + patch "/" => update_room::, + delete "/:id" => delete_room::, + get "/" => list_rooms::, + post "/query" => query_rooms:: + ), + T: Clone, GetConnection +); + +async fn get_room( + auth_session: AuthSession, + State(pool): State, + Path(id): Path, +) -> ResponseResult +where + Pool: GetConnection, +{ + authenticate( + auth_session.user.as_ref(), + [UserRole::Admin, UserRole::Receptionist], + |_| async { + Room::read(id, pool.get().await?.as_mut()) + .await + .map(Success::Ok) + .map_err(Into::into) + }, + ) + .await +} + +async fn create_room( + auth_session: AuthSession, + State(pool): State, + Valid(Json(room)): Valid>, +) -> ResponseResult +where + Pool: GetConnection, +{ + authenticate( + auth_session.user.as_ref(), + [UserRole::Admin, UserRole::Receptionist], + |_| async { + Room::insert(room, pool.get().await?.as_mut()) + .await + .map(Success::Created) + .map_err(Into::into) + }, + ) + .await +} + +async fn update_room( + auth_session: AuthSession, + State(pool): State, + Valid(Json(room)): Valid>, +) -> ResponseResult +where + Pool: GetConnection, +{ + authenticate( + auth_session.user.as_ref(), + [UserRole::Admin, UserRole::Receptionist], + |_| async { + Room::update(room, pool.get().await?.as_mut()).await?; + ok!() + }, + ) + .await +} + +async fn delete_room( + auth_session: AuthSession, + State(pool): State, + Path(id): Path, +) -> ResponseResult +where + Pool: GetConnection + Send + Sync + 'static, +{ + authenticate( + auth_session.user.as_ref(), + [UserRole::Admin, UserRole::Receptionist], + |_| async { + Room::delete(id, pool.get().await?.as_mut()).await?; + ok!() + }, + ) + .await +} + +async fn list_rooms( + auth_session: AuthSession, + State(pool): State, +) -> ResponseResult> +where + Pool: GetConnection + Send + Sync + 'static, +{ + authenticate( + auth_session.user.as_ref(), + [UserRole::Admin, UserRole::Receptionist], + |_| async { + Room::list(pool.get().await?.as_mut()) + .await + .map(Array::from) + .map(Success::Ok) + .map_err(Into::into) + }, + ) + .await +} + +async fn query_rooms( + auth_session: AuthSession, + State(pool): State, + Json(query): Json, +) -> ResponseResult> +where + Pool: GetConnection + Send + Sync + 'static, +{ + authenticate( + auth_session.user.as_ref(), + [UserRole::Admin, UserRole::Receptionist], + |_| async { + Room::query_rooms(query, pool.get().await?.as_mut()) + .await + .map(Array::from) + .map(Success::Ok) + .map_err(Into::into) + }, + ) + .await +} diff --git a/src/routes/task.rs b/src/routes/task.rs new file mode 100644 index 0000000..0accb3f --- /dev/null +++ b/src/routes/task.rs @@ -0,0 +1,155 @@ +use crate::auth::{authenticate, AuthSession}; +use crate::models::task::{CreateTask, Task}; +use crate::models::user::UserRole; +use crate::result::{ResponseResult, Success}; +use crate::{ok, GetConnection}; +use axum::extract::{Path, State}; +use axum::Json; +use axum_valid::Valid; +use lib::diesel_crud_trait::{ + DieselCrudCreate, DieselCrudDelete, DieselCrudRead, DieselCrudUpdate, +}; +use lib::{router, routes}; + +router!( + "/task", + routes!( + get "/:id" => get_task::, + post "/" => create_task::, + patch "/" => update_task::, + delete "/:id" => delete_task::, + patch "/:id/description" => set_description::, + patch "/:id/status" => set_status:: + ), + T: Clone, GetConnection +); + +#[derive(serde::Deserialize)] +struct Description { + description: String, +} + +#[derive(serde::Deserialize)] +struct TaskStatus { + status: crate::models::task::TaskStatus, +} + +async fn get_task( + auth_session: AuthSession, + State(pool): State, + Path(id): Path, +) -> ResponseResult +where + Pool: GetConnection, +{ + authenticate( + auth_session.user.as_ref(), + [UserRole::Admin, UserRole::Janitor, UserRole::Receptionist], + |_| async { + Task::read(id, pool.get().await?.as_mut()) + .await + .map(Success::Ok) + .map_err(Into::into) + }, + ) + .await +} + +async fn create_task( + auth_session: AuthSession, + State(pool): State, + Valid(Json(create_task)): Valid>, +) -> ResponseResult +where + Pool: GetConnection, +{ + authenticate( + auth_session.user.as_ref(), + [UserRole::Admin, UserRole::Janitor, UserRole::Receptionist], + |_| async { + Task::insert(create_task, pool.get().await?.as_mut()) + .await + .map(Success::Created) + .map_err(Into::into) + }, + ) + .await +} + +async fn update_task( + auth_session: AuthSession, + State(pool): State, + Valid(Json(update_task)): Valid>, +) -> ResponseResult +where + Pool: GetConnection, +{ + authenticate( + auth_session.user.as_ref(), + [UserRole::Admin, UserRole::Janitor, UserRole::Receptionist], + |_| async { + Task::update(update_task, pool.get().await?.as_mut()).await?; + ok!() + }, + ) + .await +} + +async fn delete_task( + auth_session: AuthSession, + State(pool): State, + Path(id): Path, +) -> ResponseResult +where + Pool: GetConnection, +{ + authenticate( + auth_session.user.as_ref(), + [UserRole::Admin, UserRole::Janitor, UserRole::Receptionist], + |_| async { + Task::delete(id, pool.get().await?.as_mut()).await?; + ok!() + }, + ) + .await +} + +async fn set_description( + auth_session: AuthSession, + State(pool): State, + Path(id): Path, + Json(Description { description }): Json, +) -> ResponseResult +where + Pool: GetConnection, +{ + authenticate( + auth_session.user.as_ref(), + [UserRole::Admin, UserRole::Janitor, UserRole::Receptionist], + |_| async { + Task::set_description(id, description, pool.get().await?.as_mut()).await?; + ok!() + }, + ) + .await +} + +async fn set_status( + auth_session: AuthSession, + State(pool): State, + Path(id): Path, + Json(TaskStatus { status }): Json, +) -> ResponseResult +where + Pool: GetConnection, +{ + authenticate( + auth_session.user.as_ref(), + [UserRole::Admin, UserRole::Janitor, UserRole::Receptionist], + |_| async { + Task::set_status(id, status, pool.get().await?.as_mut()).await?; + ok!() + }, + ) + .await +} diff --git a/src/routes/user.rs b/src/routes/user.rs new file mode 100644 index 0000000..3a9908b --- /dev/null +++ b/src/routes/user.rs @@ -0,0 +1,50 @@ +use crate::auth::{authenticate, AuthSession}; +use crate::models::user::{User, UserRole}; +use crate::result::{ResponseResult, Success}; +use crate::GetConnection; +use axum::extract::{Path, State}; +use lib::axum::wrappers::Array; +use lib::diesel_crud_trait::{DieselCrudList, DieselCrudRead}; +use lib::{router, routes}; + +router!( + "/user", + routes!( + get "/:email" => get_user::, + get "/" => list_users:: + ), + T: Clone, GetConnection +); +async fn get_user( + auth_session: AuthSession, + Path(email): Path, + State(pool): State, +) -> ResponseResult +where + Pool: GetConnection, +{ + authenticate(auth_session.user.as_ref(), [UserRole::Admin], |_| async { + User::read(email, pool.get().await?.as_mut()) + .await + .map(Success::Ok) + .map_err(Into::into) + }) + .await +} + +async fn list_users( + auth_session: AuthSession, + State(pool): State, +) -> ResponseResult> +where + Pool: GetConnection, +{ + authenticate(auth_session.user.as_ref(), [UserRole::Admin], |_| async { + User::list(pool.get().await?.as_mut()) + .await + .map(Array::from) + .map(Success::Ok) + .map_err(Into::into) + }) + .await +} diff --git a/src/schema.rs b/src/schema.rs new file mode 100644 index 0000000..3cc625b --- /dev/null +++ b/src/schema.rs @@ -0,0 +1,77 @@ +// @generated automatically by Diesel CLI. + +diesel::table! { + hotel (id) { + id -> Int4, + #[max_length = 255] + name -> Varchar, + #[max_length = 255] + address -> Varchar, + } +} + +diesel::table! { + reservation (id) { + id -> Int4, + room_id -> Int4, + start -> Timestamp, + end -> Timestamp, + #[max_length = 255] + user -> Varchar, + checked_in -> Bool, + } +} + +diesel::table! { + room (id) { + id -> Int4, + hotel_id -> Int4, + beds -> Int4, + size -> Int4, + } +} + +diesel::table! { + session (id) { + #[max_length = 128] + id -> Varchar, + data -> Jsonb, + expiry_date -> Timestamp, + } +} + +diesel::table! { + task (id) { + id -> Int4, + room_id -> Int4, + description -> Text, + #[max_length = 12] + status -> Varchar, + } +} + +diesel::table! { + user (email) { + #[max_length = 255] + email -> Varchar, + #[max_length = 255] + hash -> Varchar, + #[max_length = 255] + salt -> Varchar, + role -> Int2, + } +} + +diesel::joinable!(reservation -> room (room_id)); +diesel::joinable!(reservation -> user (user)); +diesel::joinable!(room -> hotel (hotel_id)); +diesel::joinable!(task -> room (room_id)); + +diesel::allow_tables_to_appear_in_same_query!( + hotel, + reservation, + room, + session, + task, + user, +); diff --git a/src/services/mod.rs b/src/services/mod.rs new file mode 100644 index 0000000..6b5da93 --- /dev/null +++ b/src/services/mod.rs @@ -0,0 +1,5 @@ +pub mod reservation_service; +pub mod room_service; +pub mod session_service; +pub mod task_service; +pub mod user_service; diff --git a/src/services/reservation_service.rs b/src/services/reservation_service.rs new file mode 100644 index 0000000..ac5b7b4 --- /dev/null +++ b/src/services/reservation_service.rs @@ -0,0 +1,322 @@ +use crate::models::reservation::Reservation; +use crate::schema::{reservation, room}; +use diesel::expression_methods::ExpressionMethods; +use diesel::result::DatabaseErrorKind; +use diesel::{BoolExpressionMethods, QueryDsl}; +use diesel_async::{AsyncPgConnection, RunQueryDsl}; +use lib::diesel_crud_trait::CrudError; +use lib::time::DateTimeInterval; +use serde::Serialize; +use thiserror::Error; + +impl Reservation { + pub async fn check_in(id: i32, conn: &mut AsyncPgConnection) -> Result<(), ReservationError> { + Self::update_check_in(id, true, conn).await + } + + pub async fn check_out(id: i32, conn: &mut AsyncPgConnection) -> Result<(), ReservationError> { + Self::update_check_in(id, false, conn).await + } + + pub async fn room_available( + room_number: i32, + DateTimeInterval { start, end }: DateTimeInterval, + conn: &mut AsyncPgConnection, + ) -> Result<(), ReservationError> { + let available_rooms: i64 = room::table + .left_outer_join(reservation::table) + .filter(room::id.eq(room_number)) + .filter( + reservation::id + .is_null() + .or(reservation::end.le(start)) + .or(reservation::start.ge(end)), + ) + .count() + .get_result(conn) + .await?; + if available_rooms == 0 { + Err(ReservationError::RoomNotAvailable) + } else { + Ok(()) + } + } + + async fn update_check_in( + id: i32, + to: bool, + conn: &mut AsyncPgConnection, + ) -> Result<(), ReservationError> { + let find_query = reservation::table.find(id); + + let reservation: Reservation = find_query.get_result(conn).await?; + if to && reservation.checked_in { + return Err(ReservationError::AlreadyCheckedIn); + } + diesel::update(find_query) + .set(reservation::checked_in.eq(to)) + .execute(conn) + .await?; + Ok(()) + } +} + +#[derive(Debug, PartialEq, Error, Serialize)] +pub enum ReservationError { + #[error("Reservation not found")] + NotFound, + #[error("Reservation already checked in")] + AlreadyCheckedIn, + #[error("Room does not exist")] + RoomNotFound, + #[error("User does not exist")] + UserNotFound, + #[error("Room is not available")] + RoomNotAvailable, + #[error("Database error: {0}")] + Other(String), +} + +impl From for ReservationError { + fn from(value: CrudError) -> Self { + match value { + CrudError::NotFound => ReservationError::NotFound, + CrudError::PoolError(pool_error) => ReservationError::Other(pool_error), + CrudError::Other(diesel_error) => diesel_error.into(), + } + } +} + +type DieselError = diesel::result::Error; + +impl From for ReservationError { + fn from(error: DieselError) -> Self { + match &error { + DieselError::NotFound => ReservationError::NotFound, + DieselError::DatabaseError(DatabaseErrorKind::ForeignKeyViolation, info) => { + if info.constraint_name().unwrap_or_default().contains("room") { + ReservationError::RoomNotFound + } else if info.constraint_name().unwrap_or_default().contains("user") { + ReservationError::UserNotFound + } else { + ReservationError::Other(error.to_string()) + } + } + _ => ReservationError::Other(error.to_string()), + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::models::hotel::{CreateHotel, Hotel}; + use crate::models::reservation::CreateReservation; + use crate::models::room::Room; + use crate::models::user::{CreateUser, User}; + use crate::schema::{hotel, room, user}; + use crate::test::setup_test_transaction; + use chrono::{Duration, Utc}; + use diesel::dsl::insert_into; + use diesel_async::AsyncPgConnection; + use lib::diesel_crud_trait::{DieselCrudCreate, DieselCrudRead}; + use secrecy::SecretString; + + #[rstest] + #[tokio::test] + async fn test_check_in(#[future] setup: Setup) { + let Setup { + mut conn, + reservation, + .. + } = setup.await; + + Reservation::check_in(reservation.id, &mut conn) + .await + .unwrap(); + + assert!( + Reservation::read(reservation.id, &mut conn) + .await + .unwrap() + .checked_in + ); + } + + #[rstest] + #[tokio::test] + async fn test_check_out(#[future] setup: Setup) { + let Setup { + mut conn, + reservation, + .. + } = setup.await; + + Reservation::check_out(reservation.id, &mut conn) + .await + .unwrap(); + + assert!( + !Reservation::read(reservation.id, &mut conn) + .await + .unwrap() + .checked_in + ); + } + + #[rstest] + #[case::available( + Duration::days(12), + Duration::days(14), + Ok(()) + )] + #[case::start_and_end_in_interval( + Duration::days(2), + Duration::days(4), + Err(ReservationError::RoomNotAvailable) + )] + #[case::start_in_interval( + Duration::days(6), + Duration::days(10), + Err(ReservationError::RoomNotAvailable) + )] + #[case::end_in_interval( + Duration::days(-2), + Duration::days(2), + Err(ReservationError::RoomNotAvailable) + )] + #[case::start_before_and_end_after_interval( + Duration::days(-2), + Duration::days(11), + Err(ReservationError::RoomNotAvailable) + )] + #[tokio::test] + async fn test_room_available( + #[future] setup: Setup, + #[case] start: Duration, + #[case] end: Duration, + #[case] expected: Result<(), ReservationError>, + ) { + let Setup { + mut conn, + reservation, + .. + } = setup.await; + + let now = Utc::now().naive_utc(); + let start = now + start; + let end = now + end; + + assert_eq!( + Reservation::room_available(reservation.room_id, (start, end).into(), &mut conn).await, + expected + ); + } + + #[rstest] + #[tokio::test] + async fn test_room_available_no_reservations(#[future] setup: Setup) { + let Setup { + mut conn, hotel, .. + } = setup.await; + + let room = Room::insert( + Room { + id: 2, + hotel_id: hotel.id, + beds: 1, + size: 1, + }, + &mut conn, + ) + .await + .unwrap(); + + let now = Utc::now().naive_utc(); + let start = now; + let end = now + Duration::days(10); + + assert_eq!( + Reservation::room_available(room.id, (start, end).into(), &mut conn).await, + Ok(()) + ); + } + + #[fixture] + async fn setup() -> Setup { + let mut conn = setup_test_transaction().await.unwrap(); + let hotel = insert_hotel(&mut conn).await; + let user = insert_user(&mut conn).await; + let room = insert_room(&mut conn, hotel.id).await; + let reservation = insert_reservation(&mut conn, room.id, user.email).await; + Setup { + conn, + hotel, + reservation, + } + } + + struct Setup { + conn: AsyncPgConnection, + hotel: Hotel, + reservation: Reservation, + } + + async fn insert_hotel(conn: &mut AsyncPgConnection) -> Hotel { + insert_into(hotel::table) + .values(CreateHotel::new( + None, + "test".to_string(), + "test".to_string(), + )) + .get_result(conn) + .await + .unwrap() + } + + async fn insert_room(conn: &mut AsyncPgConnection, hotel_id: i32) -> Room { + insert_into(room::table) + .values(Room { + id: 1, + hotel_id, + beds: 1, + size: 1, + }) + .get_result(conn) + .await + .unwrap() + } + + async fn insert_user(conn: &mut AsyncPgConnection) -> User { + insert_into(user::table) + .values( + User::hash_from_credentials(CreateUser { + email: "test_again@test.com".to_string(), + password: SecretString::new("test".to_string()), + role: None, + }) + .unwrap(), + ) + .get_result(conn) + .await + .unwrap() + } + + async fn insert_reservation( + conn: &mut AsyncPgConnection, + room_id: i32, + user: String, + ) -> Reservation { + let now = Utc::now().naive_utc(); + insert_into(reservation::table) + .values(CreateReservation { + room_id, + user, + start: now, + end: now + Duration::days(7), + }) + .get_result(conn) + .await + .unwrap() + } +} diff --git a/src/services/room_service.rs b/src/services/room_service.rs new file mode 100644 index 0000000..5a60c45 --- /dev/null +++ b/src/services/room_service.rs @@ -0,0 +1,206 @@ +use crate::error::AppError; +use crate::models::room::Room; +use crate::schema::{reservation, room}; +use diesel::expression_methods::ExpressionMethods; +use diesel::{BoolExpressionMethods, QueryDsl}; +use diesel_async::{AsyncPgConnection, RunQueryDsl}; +use lib::time::DateTimeInterval; +use serde::Deserialize; + +#[cfg_attr(test, bon::builder)] +#[derive(Clone, Copy, PartialEq, Eq, Default, Deserialize)] +pub struct RoomQuery { + pub available: Option, + pub hotel_id: Option, + pub min_beds: Option, + pub min_size: Option, +} + +impl Room { + pub async fn query_rooms( + RoomQuery { + available, + hotel_id, + min_beds, + min_size, + }: RoomQuery, + conn: &mut AsyncPgConnection, + ) -> Result, AppError> { + let mut table = room::table.left_outer_join(reservation::table).into_boxed(); + if let Some(DateTimeInterval { start, end }) = available { + table = table.filter( + reservation::id + .is_null() + .or(reservation::end.le(start)) + .or(reservation::start.ge(end)), + ) + } + if let Some(id) = hotel_id { + table = table.filter(room::hotel_id.eq(id)); + } + if let Some(beds) = min_beds { + table = table.filter(room::beds.ge(beds)); + } + if let Some(size) = min_size { + table = table.filter(room::size.ge(size)); + } + table + .select(room::all_columns) + .get_results(conn) + .await + .map_err(Into::into) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::models::hotel::{CreateHotel, Hotel}; + use crate::models::reservation::{CreateReservation, Reservation}; + use crate::models::room::Room; + use crate::models::user::{CreateUser, User}; + use crate::test::setup_test_transaction; + use diesel_async::AsyncPgConnection; + use lib::diesel_crud_trait::DieselCrudCreate; + use serde_json::json; + + #[rstest] + #[case::all_none(RoomQuery::default(), &[1, 2, 3, 4])] + #[case::min_beds_are_2( + RoomQuery::builder() + .min_beds(2) + .build(), + &[3, 4] + )] + #[case::min_beds_are_greater_than_all( + RoomQuery::builder() + .hotel_id(5) + .build(), + &[] + )] + #[case::min_size_are_5( + RoomQuery::builder() + .min_size(5) + .build(), + &[2, 4] + )] + #[case::min_size_is_greater_than_all( + RoomQuery::builder() + .min_size(10) + .build(), + &[] + )] + #[case::min_size_and_min_beds( + RoomQuery::builder() + .min_size(5) + .min_beds(2) + .build(), + &[4] + )] + #[case::interval_when_no_reservations_in_interval( + RoomQuery::builder() + .available(DateTimeInterval { + start: chrono::Utc::now().naive_utc() + chrono::Duration::days(10), + end: chrono::Utc::now().naive_utc() + chrono::Duration::days(20) + }) + .build(), + &[1, 2, 3, 4] + )] + #[case::interval_when_one_reservation_in_interval( + RoomQuery::builder() + .available(DateTimeInterval { + start: chrono::Utc::now().naive_utc(), + end: chrono::Utc::now().naive_utc() + chrono::Duration::days(10) + }) + .build(), + &[2, 3, 4] + )] + #[case::all_filters( + RoomQuery::builder() + .available(DateTimeInterval { + start: chrono::Utc::now().naive_utc(), + end: chrono::Utc::now().naive_utc() + chrono::Duration::days(10) + }) + .hotel_id(1) + .min_beds(2) + .min_size(1) + .build(), + &[3] + )] + #[tokio::test] + async fn test_query_rooms( + #[future] setup: Setup, + #[case] query: RoomQuery, + #[case] expected_ids: &[i32], + ) { + let Setup { + mut conn, rooms, .. + } = setup.await; + let result = Room::query_rooms(query, &mut conn).await.unwrap(); + assert_eq!( + result, + rooms + .into_iter() + .filter(|room| expected_ids.contains(&room.id)) + .collect::>() + ); + } + + #[fixture] + async fn setup() -> Setup { + let mut conn = setup_test_transaction().await.unwrap(); + let hotels = insert_hotels(&mut conn).await; + let rooms = insert_rooms(&mut conn, &hotels).await; + let _reservation = insert_reservation(&mut conn, rooms[0].id).await; + Setup { conn, rooms } + } + + struct Setup { + conn: AsyncPgConnection, + rooms: Vec, + } + + async fn insert_hotels(conn: &mut AsyncPgConnection) -> Vec { + let hotels = vec![ + CreateHotel::new(Some(1), "hotel1".into(), "hotel1".into()), + CreateHotel::new(Some(2), "hotel2".into(), "hotel2".into()), + ]; + Hotel::insert_many(&hotels, conn).await.unwrap() + } + + async fn insert_rooms(conn: &mut AsyncPgConnection, hotels: &[Hotel]) -> Vec { + let rooms: Vec = json!( + [ + { "beds": 1, "size": 1 }, + { "beds": 1, "size": 5 }, + { "beds": 2, "size": 1 }, + { "beds": 2, "size": 5 } + ] + ) + .as_array() + .unwrap() + .iter() + .enumerate() + .map(|(index, room)| Room { + hotel_id: hotels[index % 2].id, + beds: room["beds"].as_i64().unwrap() as i32, + size: room["size"].as_i64().unwrap() as i32, + id: index as i32 + 1, + }) + .collect(); + Room::insert_many(&rooms, conn).await.unwrap(); + rooms + } + async fn insert_reservation(conn: &mut AsyncPgConnection, room_id: i32) -> Reservation { + let user = User::insert( + User::hash_from_credentials(CreateUser::new("test@test.test", "test")).unwrap(), + conn, + ) + .await + .unwrap(); + let now = chrono::Utc::now().naive_utc(); + let reservation = + CreateReservation::new(room_id, now, now + chrono::Duration::days(5), user.email); + Reservation::insert(reservation, conn).await.unwrap() + } +} diff --git a/src/services/session_service.rs b/src/services/session_service.rs new file mode 100644 index 0000000..0b4edbe --- /dev/null +++ b/src/services/session_service.rs @@ -0,0 +1,102 @@ +use crate::error::AppError; +use crate::models::session::Session; +use crate::GetConnection; +use axum::async_trait; +use derive_more::Constructor; +use lib::diesel_crud_trait::{DieselCrudCreate, DieselCrudDelete, DieselCrudRead}; +use std::fmt::Debug; +use tower_sessions::session::{Id, Record}; +use tower_sessions::{session_store, SessionStore}; + +#[derive(Clone, Constructor)] +pub struct SessionService +where + Pool: GetConnection, +{ + pool: Pool, +} + +impl Debug for SessionService +where + Pool: GetConnection, +{ + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("SessionService") + .field("pool", &self.pool.status()) + .finish() + } +} + +impl SessionService +where + Pool: GetConnection, +{ + async fn create(&self, session: Session) -> Result { + Session::insert(session, self.pool.get().await?.as_mut()) + .await + .map_err(Into::into) + } + async fn read(&self, session_id: String) -> Result { + Session::read(session_id, self.pool.get().await?.as_mut()) + .await + .map_err(Into::into) + } + + async fn delete(&self, session_id: String) -> Result { + Session::delete(session_id, self.pool.get().await?.as_mut()) + .await + .map_err(Into::into) + } +} + +#[async_trait] +impl SessionStore for SessionService +where + Pool: GetConnection + 'static, +{ + async fn create(&self, session_record: &mut Record) -> session_store::Result<()> { + self.save(session_record).await + } + + async fn save(&self, session_record: &Record) -> session_store::Result<()> { + let Ok(model) = Session::try_from(session_record.clone()) else { + return Err(session_store::Error::Backend( + "Failed to parse record to session model".to_string(), + )); + }; + + Self::create(self, model).await.map_err(|error| { + session_store::Error::Backend(format!("Failed to save session: {}", error)) + })?; + Ok(()) + } + + async fn load(&self, session_id: &Id) -> session_store::Result> { + match self.read(session_id.0.to_string()).await { + Ok(session) => match session.try_into_record() { + Ok(record) => Ok(Some(record)), + Err(error) => Err(session_store::Error::Decode(error.to_string())), + }, + Err(AppError::NotFound) => Ok(None), + Err(error) => Err(session_store::Error::Backend(format!( + "Failed to load session: {error}", + ))), + } + } + + /// Session-fixation cycling attempts to delete the session with the old session ID. + /// If the session does not exist, it is considered a success. + async fn delete(&self, session_id: &Id) -> session_store::Result<()> { + match Self::delete(self, session_id.0.to_string()).await { + Ok(_) | Err(AppError::NotFound) => Ok(()), + Err(error) => Err(session_store::Error::Backend(format!( + "Failed to delete session: {error}", + ))), + } + } +} + +#[cfg(test)] +mod tests { + // TODO +} diff --git a/src/services/task_service.rs b/src/services/task_service.rs new file mode 100644 index 0000000..7787849 --- /dev/null +++ b/src/services/task_service.rs @@ -0,0 +1,37 @@ +use crate::error::AppError; +use crate::models::task::{Task, TaskStatus}; +use crate::schema::task; +use diesel::expression_methods::ExpressionMethods; +use diesel::QueryDsl; +use diesel_async::{AsyncPgConnection, RunQueryDsl}; + +impl Task { + pub async fn set_description( + id: i32, + description: String, + conn: &mut AsyncPgConnection, + ) -> Result<(), AppError> { + diesel::update(task::table.find(id)) + .set(task::description.eq(description)) + .execute(conn) + .await?; + Ok(()) + } + + pub async fn set_status( + id: i32, + status: TaskStatus, + conn: &mut AsyncPgConnection, + ) -> Result<(), AppError> { + diesel::update(task::table.find(id)) + .set(task::status.eq(status.to_string())) + .execute(conn) + .await?; + Ok(()) + } +} + +#[cfg(test)] +mod tests { + // TODO +} diff --git a/src/services/user_service.rs b/src/services/user_service.rs new file mode 100644 index 0000000..274232e --- /dev/null +++ b/src/services/user_service.rs @@ -0,0 +1,75 @@ +use crate::auth::hash_password; +use crate::error::{AppError, ResponseError}; +use crate::models::user::{CreateUser, User, UserCredentials}; +use crate::GetConnection; +use axum::async_trait; +use axum_login::{AuthnBackend, UserId}; +use base64ct::{Base64, Encoding}; +use derive_more::Constructor; +use lib::diesel_crud_trait::{DieselCrudCreate, DieselCrudRead}; +use secrecy::ExposeSecret; + +#[derive(Clone, Constructor)] +pub struct UserService +where + Pool: GetConnection, +{ + pool: Pool, +} + +impl UserService +where + Pool: GetConnection, +{ + pub async fn insert(&self, create: CreateUser) -> Result { + User::insert( + User::hash_from_credentials(create)?, + self.pool.get().await?.as_mut(), + ) + .await + .map_err(Into::into) + } + + async fn read(&self, email: String) -> Result { + User::read(email, self.pool.get().await?.as_mut()) + .await + .map_err(Into::into) + } +} + +#[async_trait] +impl AuthnBackend for UserService +where + Pool: GetConnection, +{ + type User = User; + type Credentials = UserCredentials; + type Error = AppError; + + async fn authenticate( + &self, + UserCredentials { email, password }: Self::Credentials, + ) -> Result, Self::Error> { + let user = self.read(email.clone()).await?; + let password = password.expose_secret(); + let mut input_hash = [0; 32]; + hash_password(password, user.salt.as_bytes(), &mut input_hash); + + let mut user_hash = [0; 32]; + Base64::decode(&user.hash, &mut user_hash)?; + if user.email == email && user_hash == input_hash { + Ok(Some(user)) + } else { + Ok(None) + } + } + + async fn get_user(&self, email: &UserId) -> Result, Self::Error> { + Ok(self.read(email.to_string()).await.ok()) + } +} + +#[cfg(test)] +mod tests { + // TODO +} diff --git a/src/test.rs b/src/test.rs new file mode 100644 index 0000000..eda1ac6 --- /dev/null +++ b/src/test.rs @@ -0,0 +1,129 @@ +use crate::config; +use crate::database::{create_pool, create_pool_from_url, GetConnection, PgPool}; +use crate::error::AppError; +use axum::async_trait; +use axum::body::{to_bytes, Body}; +use axum::http::header::CONTENT_TYPE; +use axum::http::Request; +use axum::response::Response; +use deadpool_diesel::postgres::BuildError; +use deadpool_diesel::Status; +use derive_more::Constructor; +use diesel_async::pooled_connection::deadpool::{Object, PoolError}; +use diesel_async::{AsyncConnection, AsyncPgConnection}; +use mime::APPLICATION_JSON; +use serde::{Deserialize, Serialize}; +use serde_json::json; +use testcontainers_modules::postgres::Postgres; +use testcontainers_modules::testcontainers::runners::AsyncRunner; +use testcontainers_modules::testcontainers::{ContainerAsync, TestcontainersError}; +use thiserror::Error; + +#[derive(Debug, PartialEq, Error)] +pub enum Error { + #[error(transparent)] + Connection(#[from] diesel::ConnectionError), + #[error(transparent)] + Database(#[from] diesel::result::Error), +} + +pub async fn setup_test_transaction() -> Result { + let mut conn = AsyncPgConnection::establish(config::DATABASE_URL).await?; + conn.begin_test_transaction().await?; + Ok(conn) +} + +pub(crate) async fn create_test_pool() -> Result { + let pool = create_pool()?; + Ok(PoolStub(pool)) +} + +#[derive(Debug, Error)] +pub(crate) enum ContainerError { + #[error(transparent)] + TestContainers(#[from] TestcontainersError), + #[error(transparent)] + BuildError(#[from] BuildError), + #[error(transparent)] + PoolError(#[from] PoolError), + #[error(transparent)] + DieselError(#[from] diesel::result::Error), +} + +/// When the TestContainer is dropped, the container will be removed. +/// # Panics +/// If destructed and the container field is dropped, the container will be removed, and using the pool will cause panic. +#[derive(Constructor)] +pub(crate) struct TestContainer { + pub _container: ContainerAsync, + pub pool: PgPool, +} + +pub(crate) async fn create_test_containers_pool<'a>() -> Result { + let container = create_postgres_container().await?; + let connection_string = format!( + "postgres://postgres:postgres@127.0.0.1:{}/postgres", + container.get_host_port_ipv4(5432).await? + ); + let pool = create_pool_from_url(connection_string)?; + run_migrations(pool.get().await?.as_mut()).await?; + Ok(TestContainer::new(container, pool)) +} + +pub(crate) async fn create_postgres_container( +) -> Result, TestcontainersError> { + Postgres::default().start().await +} + +pub(crate) async fn run_migrations( + conn: &mut AsyncPgConnection, +) -> Result<(), diesel::result::Error> { + config::MIGRATIONS.run_pending_migrations(conn).await +} + +#[derive(Clone)] +pub(crate) struct PoolStub(PgPool); + +#[async_trait] +impl GetConnection for PoolStub { + async fn get(&self) -> Result, AppError> { + let mut conn = self.0.get().await?; + conn.begin_test_transaction().await?; + Ok(conn) + } + fn status(&self) -> Status { + unimplemented!("PoolStub does not support status") + } +} + +pub trait BuildJson { + fn json(self, body: T) -> Result, axum::http::Error>; +} + +impl BuildJson for axum::http::request::Builder { + fn json(self, body: T) -> Result, axum::http::Error> { + self.header(CONTENT_TYPE, APPLICATION_JSON.as_ref()) + .body(Body::new(json!(body).to_string())) + } +} + +#[derive(Debug, Error)] +pub(crate) enum DeserializeError { + #[error(transparent)] + SerdeError(#[from] serde_json::Error), + #[error(transparent)] + AxumError(#[from] axum::Error), +} + +#[async_trait] +pub trait DeserializeInto { + async fn deserialize_into Deserialize<'de>>(self) -> Result; +} + +#[async_trait] +impl DeserializeInto for Response { + async fn deserialize_into Deserialize<'de>>(self) -> Result { + let body = to_bytes(self.into_body(), usize::MAX).await?; + serde_json::from_slice(&body).map_err(Into::into) + } +}