From 9cf9e5752f3753b52be4250f078d73f78e99c284 Mon Sep 17 00:00:00 2001 From: xeovalyte Date: Fri, 31 Jan 2025 13:36:09 +0100 Subject: [PATCH] Started on member migration --- devenv.nix | 6 +- server/.env | 1 + server/.gitignore | 1 + server/Cargo.lock | 507 +++++++++++++++++- server/Cargo.toml | 17 +- server/build.rs | 5 + .../001-create-user-member-session.sql | 28 - server/migrations/001_create_members.sql | 10 + server/src/auth.rs | 48 ++ server/src/auth/bearer.rs | 8 + server/src/auth/error.rs | 20 + server/src/database.rs | 2 + server/src/database/model.rs | 5 + server/src/database/model/member.rs | 54 ++ server/src/database/model/session.rs | 6 + server/src/database/model/user.rs | 8 + server/src/lib.rs | 17 + server/src/main.rs | 25 +- server/src/model.rs | 6 + server/src/model/member.rs | 46 ++ server/src/model/session.rs | 0 server/src/model/user.rs | 5 + server/src/routes.rs | 30 ++ server/src/routes/auth.rs | 1 + server/src/routes/member.rs | 1 + server/src/routes/member/migrate.rs | 260 +++++++++ server/src/routes/user.rs | 1 + server/src/util.rs | 4 + server/src/util/error.rs | 52 ++ server/src/util/helpers.rs | 6 + 30 files changed, 1122 insertions(+), 58 deletions(-) create mode 100644 server/build.rs delete mode 100644 server/migrations/001-create-user-member-session.sql create mode 100644 server/migrations/001_create_members.sql create mode 100644 server/src/auth.rs create mode 100644 server/src/auth/bearer.rs create mode 100644 server/src/auth/error.rs create mode 100644 server/src/database/model.rs create mode 100644 server/src/database/model/member.rs create mode 100644 server/src/database/model/session.rs create mode 100644 server/src/database/model/user.rs create mode 100644 server/src/model.rs create mode 100644 server/src/model/member.rs create mode 100644 server/src/model/session.rs create mode 100644 server/src/model/user.rs create mode 100644 server/src/routes.rs create mode 100644 server/src/routes/auth.rs create mode 100644 server/src/routes/member.rs create mode 100644 server/src/routes/member/migrate.rs create mode 100644 server/src/routes/user.rs create mode 100644 server/src/util.rs create mode 100644 server/src/util/error.rs create mode 100644 server/src/util/helpers.rs diff --git a/devenv.nix b/devenv.nix index 02874d9..207d05b 100644 --- a/devenv.nix +++ b/devenv.nix @@ -1,6 +1,10 @@ -{ ... }: +{ pkgs, ... }: { + packages = with pkgs; [ + openssl + ]; + languages.rust.enable = true; languages.javascript = { diff --git a/server/.env b/server/.env index 2778c4c..34416a2 100644 --- a/server/.env +++ b/server/.env @@ -1 +1,2 @@ DATABASE_URL="postgres://wrbapp:password@localhost/wrbapp" +API_TOKEN="SuperSecretToken" diff --git a/server/.gitignore b/server/.gitignore index eb5a316..c5dd462 100644 --- a/server/.gitignore +++ b/server/.gitignore @@ -1 +1,2 @@ target +.env diff --git a/server/Cargo.lock b/server/Cargo.lock index 5dd0382..4a6c987 100644 --- a/server/Cargo.lock +++ b/server/Cargo.lock @@ -17,12 +17,36 @@ version = "2.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "512761e0bb2578dd7380c6baaa0f4ce03e84f95e960231d1dec8bf4d7d6e2627" +[[package]] +name = "aho-corasick" +version = "1.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e60d3430d3a69478ad0993f19238d2df97c507009a52b3c10addcd7f6bcb916" +dependencies = [ + "memchr", +] + [[package]] name = "allocator-api2" version = "0.2.21" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923" +[[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 = "atoi" version = "2.0.0" @@ -93,6 +117,28 @@ dependencies = [ "tracing", ] +[[package]] +name = "axum-extra" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "460fc6f625a1f7705c6cf62d0d070794e94668988b1c38111baeec177c715f7b" +dependencies = [ + "axum", + "axum-core", + "bytes", + "futures-util", + "headers", + "http", + "http-body", + "http-body-util", + "mime", + "pin-project-lite", + "serde", + "tower", + "tower-layer", + "tower-service", +] + [[package]] name = "axum-macros" version = "0.5.0" @@ -119,6 +165,12 @@ dependencies = [ "windows-targets 0.52.6", ] +[[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" @@ -149,6 +201,12 @@ dependencies = [ "generic-array", ] +[[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" @@ -161,12 +219,35 @@ version = "1.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "325918d6fe32f23b19878fe4b34794ae41fc19ddbe53b10571a4874d44ffd39b" +[[package]] +name = "cc" +version = "1.2.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13208fcbb66eaeffe09b99fffbe1af420f00a7b35aa99ad683dfc1aa76145229" +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.39" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7e36cc9d416881d2e24f9a963be5fb1cd90966419ac844274161d10488b3e825" +dependencies = [ + "android-tzdata", + "iana-time-zone", + "js-sys", + "num-traits", + "wasm-bindgen", + "windows-targets 0.52.6", +] + [[package]] name = "concurrent-queue" version = "2.5.0" @@ -182,6 +263,12 @@ version = "0.9.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c2459377285ad874054d797f3ccebf984978aa39129f6eafde5cdc8315b612f8" +[[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.16" @@ -231,6 +318,62 @@ dependencies = [ "typenum", ] +[[package]] +name = "csv" +version = "1.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "acdc4883a9c96732e4733212c01447ebd805833b7275a73ca3ee080fd77afdaf" +dependencies = [ + "csv-core", + "itoa", + "ryu", + "serde", +] + +[[package]] +name = "csv-core" +version = "0.1.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5efa2b3d7902f4b634a20cae3c9c4e6209dc4779feb6863329607560143efa70" +dependencies = [ + "memchr", +] + +[[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", + "syn", +] + +[[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", +] + [[package]] name = "der" version = "0.7.9" @@ -446,7 +589,19 @@ checksum = "c4567c8db10ae91089c99af84c68c38da3ec2f087c3f82960bcdbf3656b6f4d7" dependencies = [ "cfg-if", "libc", - "wasi", + "wasi 0.11.0+wasi-snapshot-preview1", +] + +[[package]] +name = "getrandom" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "43a49c392881ce6d5c3b8cb70f98717b7c07aabbdff06687b9030dbfbe2725f8" +dependencies = [ + "cfg-if", + "libc", + "wasi 0.13.3+wasi-0.2.2", + "windows-targets 0.52.6", ] [[package]] @@ -475,6 +630,30 @@ dependencies = [ "hashbrown", ] +[[package]] +name = "headers" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "322106e6bd0cba2d5ead589ddb8150a13d7c4217cf80d7c4f682ca994ccc6aa9" +dependencies = [ + "base64 0.21.7", + "bytes", + "headers-core", + "http", + "httpdate", + "mime", + "sha1", +] + +[[package]] +name = "headers-core" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "54b4a22553d4242c49fddb9ba998a99962b5cc6f22cb5a3482bec22522403ce4" +dependencies = [ + "http", +] + [[package]] name = "heck" version = "0.5.0" @@ -595,6 +774,29 @@ dependencies = [ "tower-service", ] +[[package]] +name = "iana-time-zone" +version = "0.1.61" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "235e081f3925a06703c2d0117ea8b91f042756fd6e7a6e5d901e8ca1a996b220" +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 = "icu_collections" version = "1.5.0" @@ -713,6 +915,12 @@ dependencies = [ "syn", ] +[[package]] +name = "ident_case" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39" + [[package]] name = "idna" version = "1.0.3" @@ -744,12 +952,31 @@ dependencies = [ "hashbrown", ] +[[package]] +name = "itertools" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b192c782037fadd9cfa75548310488aabdbf3d2da73885b31bd0abd03351285" +dependencies = [ + "either", +] + [[package]] name = "itoa" version = "1.0.14" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d75a2a4b1b190afb6f5425f10f6a8f959d2ea0b9c2b1d79553551850539e4674" +[[package]] +name = "js-sys" +version = "0.3.77" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1cfaf33c695fc6e08064efbc1f72ec937429614f25eef83af942d0e227c3a28f" +dependencies = [ + "once_cell", + "wasm-bindgen", +] + [[package]] name = "lazy_static" version = "1.5.0" @@ -853,7 +1080,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2886843bf800fba2e3377cff24abf6379b4c4d5c6681eaf9ea5b0d15090450bd" dependencies = [ "libc", - "wasi", + "wasi 0.11.0+wasi-snapshot-preview1", "windows-sys 0.52.0", ] @@ -879,7 +1106,7 @@ dependencies = [ "num-integer", "num-iter", "num-traits", - "rand", + "rand 0.8.5", "smallvec", "zeroize", ] @@ -1024,7 +1251,29 @@ version = "0.2.20" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "77957b295656769bb8ad2b6a6b09d897d94f05c41b069aede1fcdaa675eaea04" dependencies = [ - "zerocopy", + "zerocopy 0.7.35", +] + +[[package]] +name = "proc-macro-error-attr2" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96de42df36bb9bba5542fe9f1a054b8cc87e172759a1868aa05c1f3acc89dfc5" +dependencies = [ + "proc-macro2", + "quote", +] + +[[package]] +name = "proc-macro-error2" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "11ec05c52be0a07b08061f7dd003e7d7092e0472bc731b4af7bb1ef876109802" +dependencies = [ + "proc-macro-error-attr2", + "proc-macro2", + "quote", + "syn", ] [[package]] @@ -1052,8 +1301,19 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" dependencies = [ "libc", - "rand_chacha", - "rand_core", + "rand_chacha 0.3.1", + "rand_core 0.6.4", +] + +[[package]] +name = "rand" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3779b94aeb87e8bd4e834cee3650289ee9e0d5677f976ecdb6d219e5f4f6cd94" +dependencies = [ + "rand_chacha 0.9.0", + "rand_core 0.9.0", + "zerocopy 0.8.14", ] [[package]] @@ -1063,7 +1323,17 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" dependencies = [ "ppv-lite86", - "rand_core", + "rand_core 0.6.4", +] + +[[package]] +name = "rand_chacha" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3022b5f1df60f26e1ffddd6c66e8aa15de382ae63b3a0c1bfc0e4d3e3f325cb" +dependencies = [ + "ppv-lite86", + "rand_core 0.9.0", ] [[package]] @@ -1072,7 +1342,17 @@ version = "0.6.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" dependencies = [ - "getrandom", + "getrandom 0.2.15", +] + +[[package]] +name = "rand_core" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b08f3c9802962f7e1b25113931d94f43ed9725bebc59db9d0c3e9a23b67e15ff" +dependencies = [ + "getrandom 0.3.1", + "zerocopy 0.8.14", ] [[package]] @@ -1084,6 +1364,35 @@ dependencies = [ "bitflags", ] +[[package]] +name = "regex" +version = "1.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b544ef1b4eac5dc2db33ea63606ae9ffcfac26c1416a2806ae0bf5f56b201191" +dependencies = [ + "aho-corasick", + "memchr", + "regex-automata", + "regex-syntax", +] + +[[package]] +name = "regex-automata" +version = "0.4.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "809e8dc61f6de73b46c85f4c96486310fe304c434cfa43669d7b40f711150908" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax", +] + +[[package]] +name = "regex-syntax" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c" + [[package]] name = "rsa" version = "0.9.7" @@ -1097,7 +1406,7 @@ dependencies = [ "num-traits", "pkcs1", "pkcs8", - "rand_core", + "rand_core 0.6.4", "signature", "spki", "subtle", @@ -1163,9 +1472,9 @@ dependencies = [ [[package]] name = "serde_json" -version = "1.0.135" +version = "1.0.137" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2b0d7ba2887406110130a978386c4e1befb98c674b4fba677954e4db976630d9" +checksum = "930cfb6e6abf99298aaad7d29abbef7a9999a9a8806a40088f55f0dcec03146b" dependencies = [ "itoa", "memchr", @@ -1226,6 +1535,12 @@ dependencies = [ "lazy_static", ] +[[package]] +name = "shlex" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" + [[package]] name = "signature" version = "2.2.0" @@ -1233,7 +1548,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "77549399552de45a898a580c1b41d445bf730df867cc44e6c0233bbc4b8329de" dependencies = [ "digest", - "rand_core", + "rand_core 0.6.4", ] [[package]] @@ -1375,7 +1690,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4560278f0e00ce64938540546f59f590d60beee33fffbd3b9cd47851e5fff233" dependencies = [ "atoi", - "base64", + "base64 0.22.1", "bitflags", "byteorder", "bytes", @@ -1397,7 +1712,7 @@ dependencies = [ "memchr", "once_cell", "percent-encoding", - "rand", + "rand 0.8.5", "rsa", "serde", "sha1", @@ -1417,7 +1732,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c5b98a57f363ed6764d5b3a12bfedf62f07aa16e1856a7ddc2a0bb190a959613" dependencies = [ "atoi", - "base64", + "base64 0.22.1", "bitflags", "byteorder", "crc", @@ -1435,7 +1750,7 @@ dependencies = [ "md-5", "memchr", "once_cell", - "rand", + "rand 0.8.5", "serde", "serde_json", "sha2", @@ -1487,6 +1802,12 @@ dependencies = [ "unicode-properties", ] +[[package]] +name = "strsim" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" + [[package]] name = "subtle" version = "2.6.1" @@ -1529,7 +1850,7 @@ checksum = "9a8a559c81686f576e8cd0290cd2a24a2a9ad80c98b3478856500fcbd7acd704" dependencies = [ "cfg-if", "fastrand", - "getrandom", + "getrandom 0.2.15", "once_cell", "rustix", "windows-sys 0.59.0", @@ -1770,6 +2091,42 @@ version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" +[[package]] +name = "uuid" +version = "1.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "744018581f9a3454a9e15beb8a33b017183f1e7c0cd170232a2d1453b23a51c4" + +[[package]] +name = "validator" +version = "0.19.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d0b4a29d8709210980a09379f27ee31549b73292c87ab9899beee1c0d3be6303" +dependencies = [ + "idna", + "once_cell", + "regex", + "serde", + "serde_derive", + "serde_json", + "url", + "validator_derive", +] + +[[package]] +name = "validator_derive" +version = "0.19.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bac855a2ce6f843beb229757e6e570a42e837bcb15e5f449dd48d5747d41bf77" +dependencies = [ + "darling", + "once_cell", + "proc-macro-error2", + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "valuable" version = "0.1.0" @@ -1794,12 +2151,79 @@ version = "0.11.0+wasi-snapshot-preview1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" +[[package]] +name = "wasi" +version = "0.13.3+wasi-0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26816d2e1a4a36a2940b96c5296ce403917633dff8f3440e9b236ed6f6bacad2" +dependencies = [ + "wit-bindgen-rt", +] + [[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.100" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1edc8929d7499fc4e8f0be2262a241556cfc54a0bea223790e71446f2aab1ef5" +dependencies = [ + "cfg-if", + "once_cell", + "rustversion", + "wasm-bindgen-macro", +] + +[[package]] +name = "wasm-bindgen-backend" +version = "0.2.100" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2f0a0651a5c2bc21487bde11ee802ccaf4c51935d0d3d42a6101f98161700bc6" +dependencies = [ + "bumpalo", + "log", + "proc-macro2", + "quote", + "syn", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.100" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7fe63fc6d09ed3792bd0897b314f53de8e16568c2b3f7982f468c0bf9bd0b407" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.100" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ae87ea40c9f689fc23f209965b6fb8a99ad69aeeb0231408be24920604395de" +dependencies = [ + "proc-macro2", + "quote", + "syn", + "wasm-bindgen-backend", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.100" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a05d73b933a847d6cccdda8f838a22ff101ad9bf93e33684f39c1f5f0eece3d" +dependencies = [ + "unicode-ident", +] + [[package]] name = "whoami" version = "1.5.2" @@ -1832,6 +2256,15 @@ 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" @@ -1980,17 +2413,35 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" +[[package]] +name = "wit-bindgen-rt" +version = "0.33.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3268f3d866458b787f390cf61f4bbb563b922d091359f9608842999eaee3943c" +dependencies = [ + "bitflags", +] + [[package]] name = "wrbapp_server" version = "0.1.0" dependencies = [ "axum", + "axum-extra", + "chrono", + "csv", "dotenvy", + "itertools", + "rand 0.9.0", "serde", + "serde_json", "sqlx", + "thiserror", "tokio", "tracing", "tracing-subscriber", + "uuid", + "validator", ] [[package]] @@ -2036,7 +2487,16 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1b9b4fd18abc82b8136838da5d50bae7bdea537c574d8dc1a34ed098d6c166f0" dependencies = [ "byteorder", - "zerocopy-derive", + "zerocopy-derive 0.7.35", +] + +[[package]] +name = "zerocopy" +version = "0.8.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a367f292d93d4eab890745e75a778da40909cab4d6ff8173693812f79c4a2468" +dependencies = [ + "zerocopy-derive 0.8.14", ] [[package]] @@ -2050,6 +2510,17 @@ dependencies = [ "syn", ] +[[package]] +name = "zerocopy-derive" +version = "0.8.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3931cb58c62c13adec22e38686b559c86a30565e16ad6e8510a337cedc611e1" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "zerofrom" version = "0.1.5" diff --git a/server/Cargo.toml b/server/Cargo.toml index e562e51..4c01c95 100644 --- a/server/Cargo.toml +++ b/server/Cargo.toml @@ -4,13 +4,26 @@ version = "0.1.0" edition = "2021" [dependencies] -axum = { version = "0.8", features = [ "macros" ] } +# Primary crates +axum = { version = "0.8", features = [ "macros", "json" ] } +axum-extra = { version = "0.10.0", features = [ "typed-header" ] } tokio = { version = "1.43", features = [ "rt-multi-thread", "macros" ] } sqlx = { version = "0.8", features = [ "runtime-tokio", "postgres" ] } +# Secondary crates +csv = { version = "1.3" } serde = "1.0" dotenvy = "0.15.7" +validator = { version = "0.19.0", features = [ "derive" ] } -# Tracing + +# Tertiary crates tracing = "0.1" tracing-subscriber = "0.3" +chrono = "0.4.39" +uuid = "1.12.0" +serde_json = "1.0.137" +rand = "0.9" +thiserror = { version = "2.0" } +itertools = "0.14" + diff --git a/server/build.rs b/server/build.rs new file mode 100644 index 0000000..d506869 --- /dev/null +++ b/server/build.rs @@ -0,0 +1,5 @@ +// generated by `sqlx migrate build-script` +fn main() { + // trigger recompilation when a new migration is added + println!("cargo:rerun-if-changed=migrations"); +} diff --git a/server/migrations/001-create-user-member-session.sql b/server/migrations/001-create-user-member-session.sql deleted file mode 100644 index 174f9fe..0000000 --- a/server/migrations/001-create-user-member-session.sql +++ /dev/null @@ -1,28 +0,0 @@ -CREATE TABLE user ( - id bigint NOT NULL PRIMARY KEY, - email text NOT NULL UNIQUE, - password text NOT NULL, - admin boolean NOT NULL -); - - -CREATE TABLE member ( - id varchar(7) NOT NULL PRIMARY KEY, - call_sign text NOT NULL, - name text NOT NULL, - registration_token text NOT NULL UNIQUE, - diploma text, - hours text[] NOT NULL, - groups text[] NOT NULL -); - - -CREATE TABLE session ( - id bigint NOT NULL PRIMARY KEY, - user_id bigint NOT NULL, - token text NOT NULL UNIQUE, - expires timestamp NOT NULL -); - - -ALTER TABLE session ADD CONSTRAINT session_user_id_fk FOREIGN KEY (user_id) REFERENCES user (id); diff --git a/server/migrations/001_create_members.sql b/server/migrations/001_create_members.sql new file mode 100644 index 0000000..c7c469f --- /dev/null +++ b/server/migrations/001_create_members.sql @@ -0,0 +1,10 @@ +create table "members" ( + id varchar(7) primary key, + first_name text not null, + full_name text not null, + registration_token text unique not null, + diploma text, + hours text[] not null, + groups text[] not null +); + diff --git a/server/src/auth.rs b/server/src/auth.rs new file mode 100644 index 0000000..4f55ba8 --- /dev/null +++ b/server/src/auth.rs @@ -0,0 +1,48 @@ +use std::collections::HashSet; + +use axum::{extract::FromRequestParts, http::request::Parts, RequestPartsExt}; +use axum_extra::{ + headers::{authorization::Bearer, Authorization}, + typed_header::TypedHeaderRejectionReason, + TypedHeader, +}; +use bearer::verify_bearer; +pub use error::AuthError; + +mod bearer; +mod error; + +#[derive(Debug)] +pub struct Permissions<'a>(pub HashSet<&'a str>); + +// Middleware for getting permissions +impl FromRequestParts for Permissions<'_> +where + S: Send + Sync, +{ + type Rejection = crate::Error; + + async fn from_request_parts(parts: &mut Parts, _state: &S) -> Result { + // First check if the request has a beaerer token to authenticate + match parts.extract::>>().await { + Ok(bearer) => { + verify_bearer(bearer.token().to_string()).map_err(|_| AuthError::InvalidToken)?; + + let permissions = Permissions { + 0: HashSet::from(["root"]), + }; + + return Ok(permissions); + } + Err(err) => match err.reason() { + TypedHeaderRejectionReason::Missing => (), + TypedHeaderRejectionReason::Error(_err) => { + return Err(AuthError::InvalidToken.into()) + } + _ => return Err(AuthError::Unexpected.into()), + }, + }; + + Err(AuthError::Unexpected.into()) + } +} diff --git a/server/src/auth/bearer.rs b/server/src/auth/bearer.rs new file mode 100644 index 0000000..380aa3a --- /dev/null +++ b/server/src/auth/bearer.rs @@ -0,0 +1,8 @@ +pub fn verify_bearer(token: String) -> Result<(), ()> { + let env_api_token = dotenvy::var("API_TOKEN").map_err(|_| ())?; + + match env_api_token == token { + true => Ok(()), + false => Err(()), + } +} diff --git a/server/src/auth/error.rs b/server/src/auth/error.rs new file mode 100644 index 0000000..18f597a --- /dev/null +++ b/server/src/auth/error.rs @@ -0,0 +1,20 @@ +use std::fmt::Display; + +#[derive(Debug)] +pub enum AuthError { + NoPermssions, + InvalidToken, + Unexpected, +} + +impl Display for AuthError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Self::NoPermssions => write!(f, "{}", "No permissions"), + Self::InvalidToken => write!(f, "{}", "Invalid token"), + Self::Unexpected => write!(f, "{}", "Unexpected error"), + } + } +} + +impl std::error::Error for AuthError {} diff --git a/server/src/database.rs b/server/src/database.rs index 5916592..25f7f97 100644 --- a/server/src/database.rs +++ b/server/src/database.rs @@ -1,3 +1,5 @@ mod postgres; pub use postgres::apply_migrations; pub use postgres::connect; + +pub mod model; diff --git a/server/src/database/model.rs b/server/src/database/model.rs new file mode 100644 index 0000000..2c64782 --- /dev/null +++ b/server/src/database/model.rs @@ -0,0 +1,5 @@ +pub mod member; +pub mod session; +pub mod user; + +pub use member::Member; diff --git a/server/src/database/model/member.rs b/server/src/database/model/member.rs new file mode 100644 index 0000000..644138f --- /dev/null +++ b/server/src/database/model/member.rs @@ -0,0 +1,54 @@ +use rand::distr::{Alphanumeric, SampleString}; +use sqlx::{Postgres, QueryBuilder}; +use validator::Validate; + +#[derive(Debug, Validate)] +pub struct Member { + #[validate(length(equal = 7))] + pub id: String, + pub first_name: String, + pub full_name: String, + pub registration_token: Option, + pub diploma: Option, + pub hours: Vec, + pub groups: Vec, +} + +impl Member { + pub async fn insert_multiple( + transaction: &mut sqlx::Transaction<'_, Postgres>, + members: Vec, + ) -> Result<(), sqlx::Error> { + let mut query_builder = QueryBuilder::new( + "INSERT INTO members(id, first_name, full_name, registration_token, diploma, hours, groups) " + ); + + query_builder.push_values(members.into_iter(), |mut b, member| { + let registration_token = Alphanumeric.sample_string(&mut rand::rng(), 16); + + b.push_bind(member.id); + b.push_bind(member.first_name); + b.push_bind(member.full_name); + b.push_bind(registration_token); + b.push_bind(member.diploma); + b.push_bind(member.hours); + b.push_bind(member.groups); + }); + + let query = query_builder.build(); + query.execute(&mut **transaction).await?; + + Ok(()) + } + + pub async fn update_multiple( + transaction: &mut sqlx::Transaction<'_, Postgres>, + members: Vec, + ) -> Result<(), sqlx::Error> { + for member in members { + sqlx::query!("UPDATE ONLY members SET first_name = $1, full_name = $2, diploma = $3, hours = $4, groups = $5 WHERE id = $6", member.first_name, member.full_name, member.diploma, &member.hours, &member.groups, member.id).execute(&mut **transaction).await?; + } + + Ok(()) + } +} diff --git a/server/src/database/model/session.rs b/server/src/database/model/session.rs new file mode 100644 index 0000000..e7d894f --- /dev/null +++ b/server/src/database/model/session.rs @@ -0,0 +1,6 @@ +struct Session { + id: u32, + user_id: u32, + token: String, + expires: chrono::NaiveDateTime, +} diff --git a/server/src/database/model/user.rs b/server/src/database/model/user.rs new file mode 100644 index 0000000..e88c096 --- /dev/null +++ b/server/src/database/model/user.rs @@ -0,0 +1,8 @@ +#[derive(validator::Validate)] +struct User { + pub id: uuid::Uuid, + #[validate(email)] + pub email: String, + pub password: String, + pub admin: bool, +} diff --git a/server/src/lib.rs b/server/src/lib.rs index 8fd0a6b..465bd83 100644 --- a/server/src/lib.rs +++ b/server/src/lib.rs @@ -1 +1,18 @@ +use routes::member::migrate::MigrationStore; +use sqlx::{Pool, Postgres}; +use std::sync::Arc; +use tokio::sync::Mutex; + +pub mod auth; pub mod database; +pub mod model; +pub mod routes; +pub mod util; + +pub use util::error::Error; + +#[derive(Clone)] +pub struct AppState { + pub pool: Pool, + pub migration_store: Arc>, +} diff --git a/server/src/main.rs b/server/src/main.rs index c4ae50b..a84fcd6 100644 --- a/server/src/main.rs +++ b/server/src/main.rs @@ -1,9 +1,13 @@ -use axum::{http::StatusCode, routing::get, Router}; -use tokio::net::TcpListener; +use std::sync::Arc; + +use axum::Router; +use tokio::{net::TcpListener, sync::Mutex}; use tracing::Level; use tracing_subscriber::FmtSubscriber; -use wrbapp_server::database; +use wrbapp_server::routes::member::migrate::MigrationStore; +use wrbapp_server::routes::routes; +use wrbapp_server::{database, AppState}; #[tokio::main] async fn main() { @@ -22,12 +26,19 @@ async fn main() { .await .expect("Database migrations failed"); - database::connect() + let pool = database::connect() .await .expect("Database connection failed"); + let migration_store = Arc::new(Mutex::new(MigrationStore::default())); + + let app_state = AppState { + pool, + migration_store, + }; + // Serve app - let app = Router::new().route("/", get(hello_world)); + let app = Router::new().merge(routes()).with_state(app_state); let listener = TcpListener::bind("127.0.0.1:3000") .await @@ -39,7 +50,3 @@ async fn main() { .await .expect("Error while serving axum application"); } - -async fn hello_world() -> Result { - Ok("Hello world".to_string()) -} diff --git a/server/src/model.rs b/server/src/model.rs new file mode 100644 index 0000000..b9bbe22 --- /dev/null +++ b/server/src/model.rs @@ -0,0 +1,6 @@ +pub mod member; +pub mod session; +pub mod user; + +pub use member::Member; +pub use user::User; diff --git a/server/src/model/member.rs b/server/src/model/member.rs new file mode 100644 index 0000000..1fd43d2 --- /dev/null +++ b/server/src/model/member.rs @@ -0,0 +1,46 @@ +#[derive(Clone, serde::Serialize)] +pub struct Name { + pub first: String, + pub full: String, +} + +#[derive(Clone, serde::Serialize)] +pub struct Member { + pub id: String, + pub name: Name, + pub registration_token: Option, + pub diploma: Option, + pub hours: Vec, + pub groups: Vec, +} + +use crate::database::model::Member as DbMember; +impl From for Member { + fn from(value: DbMember) -> Self { + Member { + id: value.id, + name: Name { + first: value.first_name, + full: value.full_name, + }, + registration_token: value.registration_token, + diploma: value.diploma, + hours: value.hours, + groups: value.groups, + } + } +} + +impl From for DbMember { + fn from(value: Member) -> Self { + DbMember { + id: value.id, + first_name: value.name.first, + full_name: value.name.full, + registration_token: None, + diploma: value.diploma, + hours: value.hours, + groups: value.groups, + } + } +} diff --git a/server/src/model/session.rs b/server/src/model/session.rs new file mode 100644 index 0000000..e69de29 diff --git a/server/src/model/user.rs b/server/src/model/user.rs new file mode 100644 index 0000000..3564290 --- /dev/null +++ b/server/src/model/user.rs @@ -0,0 +1,5 @@ +pub struct User { + pub id: uuid::Uuid, + pub email: String, + pub admin: bool, +} diff --git a/server/src/routes.rs b/server/src/routes.rs new file mode 100644 index 0000000..4e9b0b5 --- /dev/null +++ b/server/src/routes.rs @@ -0,0 +1,30 @@ +use axum::{ + extract::State, + http::StatusCode, + routing::{get, post}, + Router, +}; +use member::migrate::{migrate_confirm, migrate_request}; + +use crate::{auth::Permissions, AppState}; + +pub mod auth; +pub mod member; +pub mod user; + +pub fn routes() -> Router { + Router::new() + .route("/", get(root)) + // .route("/member/:id", get()) + .route("/members/migrate_request", post(migrate_request)) + .route("/members/migrate_confirm", post(migrate_confirm)) +} + +async fn root( + State(state): State, + permissions: Permissions<'_>, +) -> Result { + tracing::info!("{:?}", permissions); + + Ok("Hello world".to_string()) +} diff --git a/server/src/routes/auth.rs b/server/src/routes/auth.rs new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/server/src/routes/auth.rs @@ -0,0 +1 @@ + diff --git a/server/src/routes/member.rs b/server/src/routes/member.rs new file mode 100644 index 0000000..be99d40 --- /dev/null +++ b/server/src/routes/member.rs @@ -0,0 +1 @@ +pub mod migrate; diff --git a/server/src/routes/member/migrate.rs b/server/src/routes/member/migrate.rs new file mode 100644 index 0000000..aa75db7 --- /dev/null +++ b/server/src/routes/member/migrate.rs @@ -0,0 +1,260 @@ +use std::collections::HashMap; + +use axum::{extract::State, Json}; +use itertools::Itertools; +use sqlx::PgPool; + +use crate::{ + auth::{AuthError, Permissions}, + database::model::Member as DbMember, + model::{member::Name, Member}, + util::convert_vec, + AppState, +}; + +pub async fn migrate_request<'a>( + State(state): State, + permissions: Permissions<'a>, + body: String, +) -> Result, crate::Error> { + if !permissions.0.contains("root") { + return Err(AuthError::NoPermssions.into()); + } + + // Convert the input CSV to a vector of members + let members_new: Vec = Row::from_csv_multiple(&body)? + .into_iter() + .map(|m| m.into()) + .collect(); + + // TODO: Write function to get members from database + let members_old: Vec = Vec::new(); + + let members_diff = generate_diff(members_new, members_old); + + let count = state + .migration_store + .lock() + .await + .insert(members_diff.clone()); + + Ok(Json(MigrationResponse::from((count, members_diff)))) +} + +pub async fn migrate_confirm<'a>( + State(state): State, + permissions: Permissions<'a>, + body: String, +) -> Result<(), crate::Error> { + if !permissions.0.contains("root") { + return Err(AuthError::NoPermssions.into()); + } + + // TODO: Implement better error naming + let count = match body.trim().parse::() { + Ok(c) => c, + Err(_) => return Err(crate::Error::NotFound), + }; + + let mut store = state.migration_store.lock().await; + + let members_diff = match store.remove(&count) { + Some(m) => m, + None => return Err(crate::Error::NotFound), + }; + + migrate_transaction(&state.pool, members_diff).await?; + + Ok(()) +} + +async fn migrate_transaction(pool: &PgPool, members_diff: MembersDiff) -> Result<(), sqlx::Error> { + let mut transaction = pool.begin().await?; + + // DbMember::insert_multiple(&mut transaction, convert_vec(members_diff.insert)).await?; + DbMember::update_multiple(&mut transaction, convert_vec(members_diff.update)).await?; + + transaction.commit().await?; + + Ok(()) +} + +// Create a row for the csv file +#[derive(Debug, serde::Deserialize, Clone)] +struct Row { + #[serde(rename = "Relatiecode")] + id: String, + #[serde(rename = "Roepnaam")] + first_name: String, + // #[serde(rename = "Tussenvoegsel(s)")] + // middle_name: String, + // #[serde(rename = "Achternaam")] + // last_name: String, + #[serde(rename = "E-mail")] + email: String, + #[serde(rename = "Verenigingssporten")] + hours: String, + #[serde(rename = "Diploma dropdown 1")] + diploma: Option, +} + +#[derive(Clone)] +pub struct MembersDiff { + insert: Vec, + update: Vec, + remove: Vec, +} + +#[derive(serde::Serialize)] +pub struct MigrationResponse { + count: u32, + insert: Vec<(String, Name)>, + update: Vec<(String, Name)>, + remove: Vec<(String, Name)>, +} + +pub struct MigrationStore { + pub store: HashMap, + pub count: u32, +} + +impl Default for MigrationStore { + fn default() -> Self { + Self { + count: 0, + store: HashMap::new(), + } + } +} + +impl Row { + fn from_csv_multiple(input: &str) -> Result, csv::Error> { + let mut rdr = csv::ReaderBuilder::new() + .delimiter(b';') + .from_reader(input.as_bytes()); + + let members: Result, csv::Error> = rdr.deserialize().collect(); + + members + } + + fn hours_parsed(&self) -> Vec { + let mut hours: Vec = Vec::new(); + + let group_parts: Vec<&str> = self.hours.split(", ").collect(); + + for group in group_parts { + let hour_parts: Vec<&str> = group.split(" - ").collect(); + + for part in hour_parts { + if &*part != "Groep" { + hours.push(part.to_string()); + } + } + } + + hours.into_iter().unique().collect() + } +} + +impl Into for Row { + fn into(self) -> Name { + Name { + first: self.first_name, + full: "Temporarely full name".to_string(), + } + } +} + +impl Into for Row { + fn into(self) -> Member { + let name: Name = self.clone().into(); + + Member { + id: self.id.clone(), + name, + registration_token: None, + diploma: self.diploma.clone(), + hours: self.hours_parsed(), + groups: Vec::new(), + } + } +} + +impl From<(u32, MembersDiff)> for MigrationResponse { + fn from(value: (u32, MembersDiff)) -> Self { + let members_insert: Vec<(String, Name)> = + value.1.insert.into_iter().map(|m| (m.id, m.name)).collect(); + let members_update: Vec<(String, Name)> = + value.1.update.into_iter().map(|m| (m.id, m.name)).collect(); + let members_remove: Vec<(String, Name)> = + value.1.remove.into_iter().map(|m| (m.id, m.name)).collect(); + + Self { + count: value.0, + insert: members_insert, + update: members_update, + remove: members_remove, + } + } +} + +impl MigrationStore { + fn insert(&mut self, members_diff: MembersDiff) -> u32 { + let count = self.count + 1; + self.store.insert(count, members_diff); + self.count = count; + count + } + + fn get(&self, id: &u32) -> Option<&MembersDiff> { + self.store.get(id) + } + + fn remove(&mut self, id: &u32) -> Option { + self.store.remove(id) + } +} + +fn generate_diff(members_new: Vec, members_old: Vec) -> MembersDiff { + let members_old_map: HashMap = members_old + .iter() + .map(|m| (m.id.clone(), m.clone())) + .collect(); + + let members_new_map: HashMap = members_new + .iter() + .map(|m| (m.id.clone(), m.clone())) + .collect(); + + let mut members_insert: Vec = Vec::new(); + let mut members_update: Vec = Vec::new(); + let mut members_remove: Vec = Vec::new(); + + for old_member in members_old { + if let Some(new_member) = members_new_map.get(&old_member.id) { + members_update.push(Member { + id: old_member.id, + name: new_member.name.clone(), + registration_token: old_member.registration_token, + diploma: new_member.diploma.clone(), + hours: new_member.hours.clone(), + groups: old_member.groups, + }) + } else { + members_remove.push(old_member); + } + } + + for new_member in members_new { + if !members_old_map.contains_key(&new_member.id) { + members_insert.push(new_member); + } + } + + MembersDiff { + insert: members_insert, + update: members_update, + remove: members_remove, + } +} diff --git a/server/src/routes/user.rs b/server/src/routes/user.rs new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/server/src/routes/user.rs @@ -0,0 +1 @@ + diff --git a/server/src/util.rs b/server/src/util.rs new file mode 100644 index 0000000..fc84182 --- /dev/null +++ b/server/src/util.rs @@ -0,0 +1,4 @@ +pub mod error; +mod helpers; + +pub use helpers::convert_vec; diff --git a/server/src/util/error.rs b/server/src/util/error.rs new file mode 100644 index 0000000..e4bd320 --- /dev/null +++ b/server/src/util/error.rs @@ -0,0 +1,52 @@ +use crate::auth::AuthError; +use axum::{ + http::StatusCode, + response::{IntoResponse, Response}, + Json, +}; + +#[derive(thiserror::Error, Debug)] +pub enum Error { + #[error("CSV error: {0}")] + Csv(#[from] csv::Error), + + #[error("Auth error: {0}")] + Auth(#[from] crate::auth::AuthError), + + #[error("Database error: {0}")] + Database(#[from] sqlx::Error), + + #[error("Resource not found")] + NotFound, +} + +impl IntoResponse for Error { + fn into_response(self) -> Response { + tracing::error!("Error... {:?}", self); + + let (status, error_message) = match self { + Error::Auth(AuthError::NoPermssions) => { + (StatusCode::UNAUTHORIZED, String::from("No permissions")) + } + Error::Auth(AuthError::InvalidToken) => { + (StatusCode::BAD_REQUEST, String::from("Invalid token")) + } + Error::Auth(AuthError::Unexpected) => ( + StatusCode::INTERNAL_SERVER_ERROR, + String::from("Unexpected error occured"), + ), + Error::Csv(err) => (StatusCode::BAD_REQUEST, err.to_string()), + Error::NotFound => ( + StatusCode::BAD_REQUEST, + String::from("Could not find resource"), + ), + Error::Database(err) => (StatusCode::INTERNAL_SERVER_ERROR, err.to_string()), + }; + + let body = Json(serde_json::json!({ + "error": error_message + })); + + (status, body).into_response() + } +} diff --git a/server/src/util/helpers.rs b/server/src/util/helpers.rs new file mode 100644 index 0000000..60e3b77 --- /dev/null +++ b/server/src/util/helpers.rs @@ -0,0 +1,6 @@ +pub fn convert_vec(vec: Vec) -> Vec +where + U: From, +{ + vec.into_iter().map(U::from).collect() +}