Compare commits
17 Commits
4caa0c8328
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| 1c8fe09b1f | |||
| ce04aa5947 | |||
| 4251f6981d | |||
| 69652743b5 | |||
| 0d8220e147 | |||
| f91c9937a8 | |||
| 41214d8501 | |||
| 863dccd4fd | |||
| f28eb6c422 | |||
| 0dc7dfa358 | |||
| 47c37e375d | |||
| 726092e31b | |||
| 982ab52e69 | |||
| 0ecd7c8a4c | |||
| ef722fe0d8 | |||
| 58b9923eb9 | |||
| c013f6bad8 |
3
.ok
3
.ok
@@ -1,3 +0,0 @@
|
||||
db.start: docker compose up -d
|
||||
db.stop: docker compose down
|
||||
db.reset: docker compose down -v --remove-orphans
|
||||
323
Cargo.lock
generated
323
Cargo.lock
generated
@@ -1,6 +1,6 @@
|
||||
# This file is automatically @generated by Cargo.
|
||||
# It is not intended for manual editing.
|
||||
version = 3
|
||||
version = 4
|
||||
|
||||
[[package]]
|
||||
name = "addr2line"
|
||||
@@ -17,18 +17,6 @@ version = "2.0.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "512761e0bb2578dd7380c6baaa0f4ce03e84f95e960231d1dec8bf4d7d6e2627"
|
||||
|
||||
[[package]]
|
||||
name = "ahash"
|
||||
version = "0.8.11"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e89da841a80418a9b391ebaea17f5c112ffaaa96f621d2c285b5174da76b9011"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"once_cell",
|
||||
"version_check",
|
||||
"zerocopy",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "aho-corasick"
|
||||
version = "1.1.3"
|
||||
@@ -61,23 +49,28 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "anyhow"
|
||||
version = "1.0.92"
|
||||
version = "1.0.95"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "74f37166d7d48a0284b99dd824694c26119c700b53bf0d1540cdb147dbdaaf13"
|
||||
checksum = "34ac096ce696dc2fcabef30516bb13c0a68a11d30131d3df6f04711467681b04"
|
||||
|
||||
[[package]]
|
||||
name = "api"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"argon2",
|
||||
"axum",
|
||||
"chrono",
|
||||
"dotenv",
|
||||
"hmac",
|
||||
"jwt",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"sha2",
|
||||
"sqlx",
|
||||
"tokio",
|
||||
"tower",
|
||||
"tower-http",
|
||||
"utoipa",
|
||||
"utoipa-axum",
|
||||
"utoipa-swagger-ui",
|
||||
@@ -94,14 +87,15 @@ dependencies = [
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "async-trait"
|
||||
version = "0.1.83"
|
||||
name = "argon2"
|
||||
version = "0.5.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "721cae7de5c34fbb2acd27e21e6d2cf7b886dce0c27388d46c4e6c47ea4318dd"
|
||||
checksum = "3c3610892ee6e0cbce8ae2700349fcf8f98adb0dbfbee85aec3c9179d29cc072"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
"base64ct",
|
||||
"blake2",
|
||||
"cpufeatures",
|
||||
"password-hash",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -121,14 +115,14 @@ checksum = "ace50bade8e6234aa140d9a2f552bbee1db4d353f69b8217bc503490fc1a9f26"
|
||||
|
||||
[[package]]
|
||||
name = "axum"
|
||||
version = "0.7.9"
|
||||
version = "0.8.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "edca88bc138befd0323b20752846e6587272d3b03b0343c8ea28a6f819e6e71f"
|
||||
checksum = "6d6fd624c75e18b3b4c6b9caf42b1afe24437daaee904069137d8bab077be8b8"
|
||||
dependencies = [
|
||||
"async-trait",
|
||||
"axum-core",
|
||||
"axum-macros",
|
||||
"bytes",
|
||||
"form_urlencoded",
|
||||
"futures-util",
|
||||
"http",
|
||||
"http-body",
|
||||
@@ -146,7 +140,7 @@ dependencies = [
|
||||
"serde_json",
|
||||
"serde_path_to_error",
|
||||
"serde_urlencoded",
|
||||
"sync_wrapper 1.0.1",
|
||||
"sync_wrapper",
|
||||
"tokio",
|
||||
"tower",
|
||||
"tower-layer",
|
||||
@@ -156,11 +150,10 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "axum-core"
|
||||
version = "0.4.5"
|
||||
version = "0.5.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "09f2bd6146b97ae3359fa0cc6d6b376d9539582c7b4220f041a33ec24c226199"
|
||||
checksum = "df1362f362fd16024ae199c1970ce98f9661bf5ef94b9808fee734bc3698b733"
|
||||
dependencies = [
|
||||
"async-trait",
|
||||
"bytes",
|
||||
"futures-util",
|
||||
"http",
|
||||
@@ -169,7 +162,7 @@ dependencies = [
|
||||
"mime",
|
||||
"pin-project-lite",
|
||||
"rustversion",
|
||||
"sync_wrapper 1.0.1",
|
||||
"sync_wrapper",
|
||||
"tower-layer",
|
||||
"tower-service",
|
||||
"tracing",
|
||||
@@ -177,9 +170,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "axum-macros"
|
||||
version = "0.4.2"
|
||||
version = "0.5.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "57d123550fa8d071b7255cb0cc04dc302baa6c8c4a79f55701552684d8399bce"
|
||||
checksum = "604fde5e028fea851ce1d8570bbdc034bec850d157f7569d10f347d06808c05c"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
@@ -201,6 +194,12 @@ dependencies = [
|
||||
"windows-targets 0.52.6",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "base64"
|
||||
version = "0.13.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9e1b586273c5702936fe7b7d6896644d8be71e6314cfe09d3167c95f712589e8"
|
||||
|
||||
[[package]]
|
||||
name = "base64"
|
||||
version = "0.22.1"
|
||||
@@ -222,6 +221,15 @@ dependencies = [
|
||||
"serde",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "blake2"
|
||||
version = "0.10.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "46502ad458c9a52b69d4d4d32775c788b7a1b85e8bc9d482d92250fc0e3f8efe"
|
||||
dependencies = [
|
||||
"digest",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "block-buffer"
|
||||
version = "0.10.4"
|
||||
@@ -274,6 +282,7 @@ dependencies = [
|
||||
"iana-time-zone",
|
||||
"js-sys",
|
||||
"num-traits",
|
||||
"serde",
|
||||
"wasm-bindgen",
|
||||
"windows-targets 0.52.6",
|
||||
]
|
||||
@@ -494,6 +503,12 @@ version = "1.0.7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1"
|
||||
|
||||
[[package]]
|
||||
name = "foldhash"
|
||||
version = "0.1.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a0d2fde1f7b3d48b8395d5f2de76c18a528bd6a9cdde438df747bfcba3e05d6f"
|
||||
|
||||
[[package]]
|
||||
name = "form_urlencoded"
|
||||
version = "1.2.1"
|
||||
@@ -593,7 +608,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]]
|
||||
@@ -602,29 +629,24 @@ version = "0.31.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "07e28edb80900c19c28f1072f2e8aeca7fa06b23cd4169cefe1af5aa3260783f"
|
||||
|
||||
[[package]]
|
||||
name = "hashbrown"
|
||||
version = "0.14.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1"
|
||||
dependencies = [
|
||||
"ahash",
|
||||
"allocator-api2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "hashbrown"
|
||||
version = "0.15.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1e087f84d4f86bf4b218b927129862374b72199ae7d8657835f1e89000eea4fb"
|
||||
dependencies = [
|
||||
"allocator-api2",
|
||||
"equivalent",
|
||||
"foldhash",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "hashlink"
|
||||
version = "0.9.1"
|
||||
version = "0.10.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6ba4ff7128dee98c7dc9794b6a411377e1404dba1c97deb8d1a55297bd25d8af"
|
||||
checksum = "7382cf6263419f2d8df38c55d7da83da5c18aef87fc7a7fc1fb1e344edfe14c1"
|
||||
dependencies = [
|
||||
"hashbrown 0.14.5",
|
||||
"hashbrown",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -793,7 +815,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "707907fe3c25f5424cce2cb7e1cbcafee6bdbe735ca90ef77c29e84591e5b9da"
|
||||
dependencies = [
|
||||
"equivalent",
|
||||
"hashbrown 0.15.0",
|
||||
"hashbrown",
|
||||
"serde",
|
||||
]
|
||||
|
||||
@@ -813,6 +835,21 @@ dependencies = [
|
||||
"wasm-bindgen",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "jwt"
|
||||
version = "0.16.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6204285f77fe7d9784db3fdc449ecce1a0114927a51d5a41c4c7a292011c015f"
|
||||
dependencies = [
|
||||
"base64 0.13.1",
|
||||
"crypto-common",
|
||||
"digest",
|
||||
"hmac",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"sha2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "lazy_static"
|
||||
version = "1.5.0"
|
||||
@@ -840,7 +877,6 @@ version = "0.30.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2e99fb7a497b1e3339bc746195567ed8d3e24945ecd636e3619d20b9de9e9149"
|
||||
dependencies = [
|
||||
"cc",
|
||||
"pkg-config",
|
||||
"vcpkg",
|
||||
]
|
||||
@@ -875,9 +911,9 @@ checksum = "a7a70ba024b9dc04c27ea2f0c0548feb474ec5c54bba33a7f72f873a39d07b24"
|
||||
|
||||
[[package]]
|
||||
name = "matchit"
|
||||
version = "0.7.3"
|
||||
version = "0.8.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0e7465ac9959cc2b1404e8e2367b43684a6d13790fe23056cc8c6c5a6b7bcb94"
|
||||
checksum = "47e1ffaa40ddd1f3ed91f717a33c8c0ee23fff369e3aa8772b9605cc1d22f4c3"
|
||||
|
||||
[[package]]
|
||||
name = "md-5"
|
||||
@@ -911,12 +947,6 @@ dependencies = [
|
||||
"unicase",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "minimal-lexical"
|
||||
version = "0.2.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a"
|
||||
|
||||
[[package]]
|
||||
name = "miniz_oxide"
|
||||
version = "0.8.0"
|
||||
@@ -934,20 +964,10 @@ checksum = "80e04d1dcff3aae0704555fe5fee3bcfaf3d1fdf8a7e521d5b9d2b42acb52cec"
|
||||
dependencies = [
|
||||
"hermit-abi",
|
||||
"libc",
|
||||
"wasi",
|
||||
"wasi 0.11.0+wasi-snapshot-preview1",
|
||||
"windows-sys 0.52.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "nom"
|
||||
version = "7.1.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d273983c5a657a70a3e8f2a01329822f3b8c8172b73826411a55751e404a0a4a"
|
||||
dependencies = [
|
||||
"memchr",
|
||||
"minimal-lexical",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "num-bigint-dig"
|
||||
version = "0.8.4"
|
||||
@@ -1039,6 +1059,17 @@ dependencies = [
|
||||
"windows-targets 0.52.6",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "password-hash"
|
||||
version = "0.5.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "346f04948ba92c43e8469c1ee6736c7563d71012b17d40745260fe106aac2166"
|
||||
dependencies = [
|
||||
"base64ct",
|
||||
"rand_core",
|
||||
"subtle",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "paste"
|
||||
version = "1.0.15"
|
||||
@@ -1153,7 +1184,7 @@ version = "0.6.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c"
|
||||
dependencies = [
|
||||
"getrandom",
|
||||
"getrandom 0.2.15",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -1202,7 +1233,7 @@ checksum = "c17fa4cb658e3583423e915b9f3acc01cceaee1860e33d59ebae66adc3a2dc0d"
|
||||
dependencies = [
|
||||
"cc",
|
||||
"cfg-if",
|
||||
"getrandom",
|
||||
"getrandom 0.2.15",
|
||||
"libc",
|
||||
"spin",
|
||||
"untrusted",
|
||||
@@ -1503,21 +1534,11 @@ dependencies = [
|
||||
"der",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "sqlformat"
|
||||
version = "0.2.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7bba3a93db0cc4f7bdece8bb09e77e2e785c20bfebf79eb8340ed80708048790"
|
||||
dependencies = [
|
||||
"nom",
|
||||
"unicode_categories",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "sqlx"
|
||||
version = "0.8.2"
|
||||
version = "0.8.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "93334716a037193fac19df402f8571269c84a00852f6a7066b5d2616dcd64d3e"
|
||||
checksum = "4410e73b3c0d8442c5f99b425d7a435b5ee0ae4167b3196771dd3f7a01be745f"
|
||||
dependencies = [
|
||||
"sqlx-core",
|
||||
"sqlx-macros",
|
||||
@@ -1528,31 +1549,26 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "sqlx-core"
|
||||
version = "0.8.2"
|
||||
version = "0.8.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d4d8060b456358185f7d50c55d9b5066ad956956fddec42ee2e8567134a8936e"
|
||||
checksum = "6a007b6936676aa9ab40207cde35daab0a04b823be8ae004368c0793b96a61e0"
|
||||
dependencies = [
|
||||
"atoi",
|
||||
"byteorder",
|
||||
"bytes",
|
||||
"chrono",
|
||||
"crc",
|
||||
"crossbeam-queue",
|
||||
"either",
|
||||
"event-listener",
|
||||
"futures-channel",
|
||||
"futures-core",
|
||||
"futures-intrusive",
|
||||
"futures-io",
|
||||
"futures-util",
|
||||
"hashbrown 0.14.5",
|
||||
"hashbrown",
|
||||
"hashlink",
|
||||
"hex",
|
||||
"indexmap",
|
||||
"log",
|
||||
"memchr",
|
||||
"once_cell",
|
||||
"paste",
|
||||
"percent-encoding",
|
||||
"rustls",
|
||||
"rustls-pemfile",
|
||||
@@ -1560,8 +1576,7 @@ dependencies = [
|
||||
"serde_json",
|
||||
"sha2",
|
||||
"smallvec",
|
||||
"sqlformat",
|
||||
"thiserror",
|
||||
"thiserror 2.0.11",
|
||||
"tokio",
|
||||
"tokio-stream",
|
||||
"tracing",
|
||||
@@ -1572,9 +1587,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "sqlx-macros"
|
||||
version = "0.8.2"
|
||||
version = "0.8.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "cac0692bcc9de3b073e8d747391827297e075c7710ff6276d9f7a1f3d58c6657"
|
||||
checksum = "3112e2ad78643fef903618d78cf0aec1cb3134b019730edb039b69eaf531f310"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
@@ -1585,9 +1600,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "sqlx-macros-core"
|
||||
version = "0.8.2"
|
||||
version = "0.8.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1804e8a7c7865599c9c79be146dc8a9fd8cc86935fa641d3ea58e5f0688abaa5"
|
||||
checksum = "4e9f90acc5ab146a99bf5061a7eb4976b573f560bc898ef3bf8435448dd5e7ad"
|
||||
dependencies = [
|
||||
"dotenvy",
|
||||
"either",
|
||||
@@ -1611,12 +1626,12 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "sqlx-mysql"
|
||||
version = "0.8.2"
|
||||
version = "0.8.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "64bb4714269afa44aef2755150a0fc19d756fb580a67db8885608cf02f47d06a"
|
||||
checksum = "4560278f0e00ce64938540546f59f590d60beee33fffbd3b9cd47851e5fff233"
|
||||
dependencies = [
|
||||
"atoi",
|
||||
"base64",
|
||||
"base64 0.22.1",
|
||||
"bitflags",
|
||||
"byteorder",
|
||||
"bytes",
|
||||
@@ -1647,7 +1662,7 @@ dependencies = [
|
||||
"smallvec",
|
||||
"sqlx-core",
|
||||
"stringprep",
|
||||
"thiserror",
|
||||
"thiserror 2.0.11",
|
||||
"tracing",
|
||||
"uuid",
|
||||
"whoami",
|
||||
@@ -1655,12 +1670,12 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "sqlx-postgres"
|
||||
version = "0.8.2"
|
||||
version = "0.8.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6fa91a732d854c5d7726349bb4bb879bb9478993ceb764247660aee25f67c2f8"
|
||||
checksum = "c5b98a57f363ed6764d5b3a12bfedf62f07aa16e1856a7ddc2a0bb190a959613"
|
||||
dependencies = [
|
||||
"atoi",
|
||||
"base64",
|
||||
"base64 0.22.1",
|
||||
"bitflags",
|
||||
"byteorder",
|
||||
"chrono",
|
||||
@@ -1669,7 +1684,6 @@ dependencies = [
|
||||
"etcetera",
|
||||
"futures-channel",
|
||||
"futures-core",
|
||||
"futures-io",
|
||||
"futures-util",
|
||||
"hex",
|
||||
"hkdf",
|
||||
@@ -1687,7 +1701,7 @@ dependencies = [
|
||||
"smallvec",
|
||||
"sqlx-core",
|
||||
"stringprep",
|
||||
"thiserror",
|
||||
"thiserror 2.0.11",
|
||||
"tracing",
|
||||
"uuid",
|
||||
"whoami",
|
||||
@@ -1695,9 +1709,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "sqlx-sqlite"
|
||||
version = "0.8.2"
|
||||
version = "0.8.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d5b2cf34a45953bfd3daaf3db0f7a7878ab9b7a6b91b422d24a7a9e4c857b680"
|
||||
checksum = "f85ca71d3a5b24e64e1d08dd8fe36c6c95c339a896cc33068148906784620540"
|
||||
dependencies = [
|
||||
"atoi",
|
||||
"chrono",
|
||||
@@ -1746,12 +1760,6 @@ dependencies = [
|
||||
"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"
|
||||
@@ -1777,7 +1785,16 @@ version = "1.0.66"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5d171f59dbaa811dbbb1aee1e73db92ec2b122911a48e1390dfe327a821ddede"
|
||||
dependencies = [
|
||||
"thiserror-impl",
|
||||
"thiserror-impl 1.0.66",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "thiserror"
|
||||
version = "2.0.11"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d452f284b73e6d76dd36758a0c8684b1d5be31f92b89d07fd5822175732206fc"
|
||||
dependencies = [
|
||||
"thiserror-impl 2.0.11",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -1791,6 +1808,17 @@ dependencies = [
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "thiserror-impl"
|
||||
version = "2.0.11"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "26afc1baea8a989337eeb52b6e72a039780ce45c3edfcc9c5b9d112feeb173c2"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tinyvec"
|
||||
version = "1.8.0"
|
||||
@@ -1848,20 +1876,34 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "tower"
|
||||
version = "0.5.1"
|
||||
version = "0.5.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2873938d487c3cfb9aed7546dc9f2711d867c9f90c46b889989a2cb84eba6b4f"
|
||||
checksum = "d039ad9159c98b70ecfd540b2573b97f7f52c3e8d9f8ad57a24b916a536975f9"
|
||||
dependencies = [
|
||||
"futures-core",
|
||||
"futures-util",
|
||||
"pin-project-lite",
|
||||
"sync_wrapper 0.1.2",
|
||||
"sync_wrapper",
|
||||
"tokio",
|
||||
"tower-layer",
|
||||
"tower-service",
|
||||
"tracing",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tower-http"
|
||||
version = "0.6.8"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d4e6559d53cc268e5031cd8429d05415bc4cb4aefc4aa5d6cc35fbf5b924a1f8"
|
||||
dependencies = [
|
||||
"bitflags",
|
||||
"bytes",
|
||||
"http",
|
||||
"pin-project-lite",
|
||||
"tower-layer",
|
||||
"tower-service",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tower-layer"
|
||||
version = "0.3.3"
|
||||
@@ -1945,12 +1987,6 @@ version = "0.1.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e70f2a8b45122e719eb623c01822704c4e0907e7e426a05927e1a1cfff5b75d0"
|
||||
|
||||
[[package]]
|
||||
name = "unicode_categories"
|
||||
version = "0.1.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "39ec24b3121d976906ece63c9daad25b85969647682eee313cb5779fdd69e14e"
|
||||
|
||||
[[package]]
|
||||
name = "untrusted"
|
||||
version = "0.9.0"
|
||||
@@ -1970,9 +2006,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "utoipa"
|
||||
version = "5.2.0"
|
||||
version = "5.3.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "514a48569e4e21c86d0b84b5612b5e73c0b2cf09db63260134ba426d4e8ea714"
|
||||
checksum = "435c6f69ef38c9017b4b4eea965dfb91e71e53d869e896db40d1cf2441dd75c0"
|
||||
dependencies = [
|
||||
"indexmap",
|
||||
"serde",
|
||||
@@ -1982,9 +2018,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "utoipa-axum"
|
||||
version = "0.1.2"
|
||||
version = "0.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1370cc4a8eee751c4d2a729566d83d1568212320a20581c7c72c2d76ab80ed37"
|
||||
checksum = "7c25bae5bccc842449ec0c5ddc5cbb6a3a1eaeac4503895dc105a1138f8234a0"
|
||||
dependencies = [
|
||||
"axum",
|
||||
"paste",
|
||||
@@ -1995,9 +2031,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "utoipa-gen"
|
||||
version = "5.2.0"
|
||||
version = "5.3.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5629efe65599d0ccd5d493688cbf6e03aa7c1da07fe59ff97cf5977ed0637f66"
|
||||
checksum = "a77d306bc75294fd52f3e99b13ece67c02c1a2789190a6f31d32f736624326f7"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
@@ -2007,11 +2043,12 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "utoipa-swagger-ui"
|
||||
version = "8.0.3"
|
||||
version = "9.0.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a5c80b4dd79ea382e8374d67dcce22b5c6663fa13a82ad3886441d1bbede5e35"
|
||||
checksum = "161166ec520c50144922a625d8bc4925cc801b2dda958ab69878527c0e5c5d61"
|
||||
dependencies = [
|
||||
"axum",
|
||||
"base64 0.22.1",
|
||||
"mime_guess",
|
||||
"regex",
|
||||
"rust-embed",
|
||||
@@ -2024,9 +2061,13 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "uuid"
|
||||
version = "1.11.0"
|
||||
version = "1.13.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f8c5f0a0af699448548ad1a2fbf920fb4bee257eae39953ba95cb84891a0446a"
|
||||
checksum = "ced87ca4be083373936a67f8de945faa23b6b42384bd5b64434850802c6dccd0"
|
||||
dependencies = [
|
||||
"getrandom 0.3.1",
|
||||
"serde",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "vcpkg"
|
||||
@@ -2056,6 +2097,15 @@ 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"
|
||||
@@ -2301,6 +2351,15 @@ 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 = "zerocopy"
|
||||
version = "0.7.35"
|
||||
@@ -2341,7 +2400,7 @@ dependencies = [
|
||||
"flate2",
|
||||
"indexmap",
|
||||
"memchr",
|
||||
"thiserror",
|
||||
"thiserror 1.0.66",
|
||||
"zopfli",
|
||||
]
|
||||
|
||||
|
||||
21
Cargo.toml
21
Cargo.toml
@@ -4,22 +4,27 @@ version = "0.1.0"
|
||||
edition = "2021"
|
||||
|
||||
[dependencies]
|
||||
axum = { version = "0.7.9", features = ["macros", "json"] }
|
||||
axum = { version = "0.8.1", features = ["macros", "json"] }
|
||||
tokio = { version = "1", features = ["full"] }
|
||||
tower = "0.5"
|
||||
utoipa = { version = "5.2.0", features = ["axum_extras"] }
|
||||
utoipa-swagger-ui = { version = "8.0.3", features = ["axum"] }
|
||||
utoipa-axum = "0.1.2"
|
||||
tower-http = { version = "0.6.2", features = ["cors"] }
|
||||
utoipa = { version = "5.3.1", features = ["axum_extras"] }
|
||||
utoipa-swagger-ui = { version = "9.0.0", features = ["axum"] }
|
||||
utoipa-axum = "0.2.0"
|
||||
serde = "1"
|
||||
serde_json = "1"
|
||||
anyhow = "1.0.92"
|
||||
sqlx = { version = "0.8.2", features = [
|
||||
anyhow = "1.0.95"
|
||||
sqlx = { version = "0.8.3", features = [
|
||||
"postgres",
|
||||
"chrono",
|
||||
"runtime-tokio-rustls",
|
||||
"macros",
|
||||
"uuid",
|
||||
] }
|
||||
uuid = "1.11.0"
|
||||
chrono = "0.4.39"
|
||||
uuid = { version = "1.13.1", features = ["serde", "v4"] }
|
||||
chrono = { version = "0.4.39", features = ["serde"] }
|
||||
dotenv = "0.15.0"
|
||||
argon2 = "0.5.3"
|
||||
jwt = "0.16.0"
|
||||
hmac = "0.12.1"
|
||||
sha2 = "0.10.8"
|
||||
|
||||
15
Justfile
Normal file
15
Justfile
Normal file
@@ -0,0 +1,15 @@
|
||||
mod db
|
||||
_default:
|
||||
just --list
|
||||
|
||||
migrate:
|
||||
sqlx migrate run
|
||||
|
||||
build:
|
||||
cargo build --release
|
||||
|
||||
run:
|
||||
cargo run
|
||||
|
||||
dev:
|
||||
cargo watch -x run -i "openapi.json"
|
||||
3180
data/data.txt
Normal file
3180
data/data.txt
Normal file
File diff suppressed because it is too large
Load Diff
3458
data/muscle-data.json
Normal file
3458
data/muscle-data.json
Normal file
File diff suppressed because it is too large
Load Diff
73
data/parse.ts
Normal file
73
data/parse.ts
Normal file
@@ -0,0 +1,73 @@
|
||||
import * as fs from 'fs';
|
||||
|
||||
interface MuscleEntry {
|
||||
muscle: string;
|
||||
location: string;
|
||||
origin: string;
|
||||
insertion: string;
|
||||
artery: string;
|
||||
nerve: string;
|
||||
action: string;
|
||||
antagonist: string;
|
||||
o: number;
|
||||
ta: number;
|
||||
}
|
||||
|
||||
function parseMuscleTable(table: string): MuscleEntry[] {
|
||||
const rows = table.split(/\|- style="vertical-align: top;"/).slice(1); // Split rows and ignore header
|
||||
|
||||
return rows.map(row => {
|
||||
const columns = row.split('\n').map(chopUntilPipe);
|
||||
columns.shift();
|
||||
// .replace(/\{\{[^}]+\}\}/g, '') // Remove templates like {{...}}
|
||||
// .replace(/<[^>]+>/g, '') // Remove HTML tags
|
||||
// .split('|').slice(1); // Split columns and ignore the first empty element
|
||||
|
||||
return {
|
||||
muscle: extractText(columns[0]),
|
||||
location: extractText(columns[1]),
|
||||
origin: extractText(columns[2]),
|
||||
insertion: extractText(columns[3]),
|
||||
artery: extractText(columns[4]),
|
||||
nerve: extractText(columns[5]),
|
||||
action: extractText(columns[6]),
|
||||
antagonist: extractText(columns[7] || '?'),
|
||||
o: parseInt(columns[8] || '0', 10),
|
||||
ta: parseInt(columns[9] || '0', 10),
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
function extractText(input: string): string {
|
||||
return input
|
||||
.replace(/\[\[(.*?)\|(.*?)\]\]/g, "$2")
|
||||
.replace(/\[\[(.*?)\]\]/g, "$1")
|
||||
.replace(/\<ref[^>]*>.*?<\/ref>/g, '') // Remove references like <ref>...</ref>
|
||||
.trim();
|
||||
}
|
||||
|
||||
|
||||
function chopUntilPipe(input: string): string {
|
||||
const pipeIndex = input.indexOf('|');
|
||||
return pipeIndex >= 0 ? input.slice(pipeIndex + 1) : input;
|
||||
}
|
||||
|
||||
// Read data from file
|
||||
fs.readFile('data.txt', 'utf8', (err, data) => {
|
||||
if (err) {
|
||||
console.error('Error reading file:', err);
|
||||
return;
|
||||
}
|
||||
|
||||
const parsedData = parseMuscleTable(data);
|
||||
// console.log(parsedData[0]);
|
||||
console.log(parsedData);
|
||||
|
||||
fs.writeFile('muscle-data.json', JSON.stringify(parsedData, null, 2), (err) => {
|
||||
if (err) {
|
||||
console.error('Error writing file:', err);
|
||||
return;
|
||||
}
|
||||
console.log('Data written to muscle-data.json');
|
||||
});
|
||||
});
|
||||
9
data/test.ts
Normal file
9
data/test.ts
Normal file
@@ -0,0 +1,9 @@
|
||||
const gae = "[[Occipitalis muscle|occipitalis]]";
|
||||
|
||||
const gae2 = "occipitalis";
|
||||
|
||||
function clean(str: string) {
|
||||
return str.replace(/\[\[(.*?)\|(.*?)\]\]/g, "$2");
|
||||
}
|
||||
|
||||
console.log(clean(gae));
|
||||
9
db.just
Normal file
9
db.just
Normal file
@@ -0,0 +1,9 @@
|
||||
_default:
|
||||
just --list db
|
||||
|
||||
start:
|
||||
docker compose up -d
|
||||
stop:
|
||||
docker compose down
|
||||
reset:
|
||||
docker compose down -v --remove-orphans
|
||||
@@ -1,7 +1,8 @@
|
||||
name: uplifting-api
|
||||
services:
|
||||
postgres:
|
||||
image: postgres:latest
|
||||
container_name: postgres-uplifting-api
|
||||
container_name: postgres
|
||||
ports:
|
||||
- "5401:5432"
|
||||
environment:
|
||||
@@ -9,7 +10,7 @@ services:
|
||||
POSTGRES_PASSWORD: postgres
|
||||
POSTGRES_DB: postgres
|
||||
volumes:
|
||||
- ~/databases/uplifting-api-postgres-data:/var/lib/postgresql/data
|
||||
- pg-data:/var/lib/postgresql
|
||||
|
||||
volumes:
|
||||
postgres-data:
|
||||
pg-data:
|
||||
|
||||
111
gen.ts
Normal file
111
gen.ts
Normal file
@@ -0,0 +1,111 @@
|
||||
// Define the structure of your data
|
||||
interface Muscle {
|
||||
colloquial_name: string;
|
||||
scientific_name: string;
|
||||
}
|
||||
|
||||
interface MuscleGroup {
|
||||
major_group: string;
|
||||
group_name: string;
|
||||
muscles: Muscle[];
|
||||
}
|
||||
|
||||
// Your JSON data
|
||||
const data: MuscleGroup[] = [
|
||||
{
|
||||
major_group: "Upper Body",
|
||||
group_name: "Chest",
|
||||
muscles: [
|
||||
{ colloquial_name: "Upper Pecs", scientific_name: "Clavicular Pectoralis Major" },
|
||||
{ colloquial_name: "Pecs", scientific_name: "Sternal Pectoralis Major" },
|
||||
]
|
||||
},
|
||||
{
|
||||
major_group: "Upper Body",
|
||||
group_name: "Back",
|
||||
muscles: [
|
||||
{ colloquial_name: "Lats", scientific_name: "Latissimus dorsi" },
|
||||
{ colloquial_name: "Traps", scientific_name: "Trapezius" },
|
||||
{ colloquial_name: "Rhomboids", scientific_name: "Rhomboideus major" },
|
||||
{ colloquial_name: "Spinal Erectors", scientific_name: "Erector spinae" },
|
||||
{ colloquial_name: "Teres Major", scientific_name: "Teres major" }
|
||||
]
|
||||
},
|
||||
{
|
||||
major_group: "Upper Body",
|
||||
group_name: "Shoulders",
|
||||
muscles: [
|
||||
{ colloquial_name: "Front Delts", scientific_name: "Anterior deltoid" },
|
||||
{ colloquial_name: "Rear Delts", scientific_name: "Posterior deltoid" },
|
||||
{ colloquial_name: "Side Delts", scientific_name: "Lateral deltoid" },
|
||||
{ colloquial_name: "Rotator Cuff", scientific_name: "Rotator cuff" }
|
||||
]
|
||||
},
|
||||
{
|
||||
major_group: "Upper Body",
|
||||
group_name: "Arms",
|
||||
muscles: [
|
||||
{ colloquial_name: "Biceps", scientific_name: "Biceps brachii" },
|
||||
{ colloquial_name: "Brachialis", scientific_name: "Brachialis" },
|
||||
// TODO: add all 3 tricep heads, since you can easily bias them
|
||||
{ colloquial_name: "Triceps", scientific_name: "Triceps brachii" },
|
||||
{ colloquial_name: "Brachioradialis", scientific_name: "Brachioradialis" },
|
||||
{ colloquial_name: "Forearm Flexors/Extensors", scientific_name: "Flexor and extensor muscles of the forearm" }
|
||||
]
|
||||
},
|
||||
{
|
||||
major_group: "Core",
|
||||
group_name: "Abdominals",
|
||||
muscles: [
|
||||
// TODO: add upper and lower core muscles
|
||||
{ colloquial_name: "Abs", scientific_name: "Rectus abdominis" },
|
||||
{ colloquial_name: "Transverse Abs", scientific_name: "Transversus abdominis" },
|
||||
{ colloquial_name: "External Obliques", scientific_name: "Obliquus externus abdominis" },
|
||||
{ colloquial_name: "Internal Obliques", scientific_name: "Obliquus internus abdominis" }
|
||||
]
|
||||
},
|
||||
{
|
||||
major_group: "Core",
|
||||
group_name: "Lower Back",
|
||||
muscles: [
|
||||
{ colloquial_name: "QL", scientific_name: "Quadratus lumborum" },
|
||||
]
|
||||
},
|
||||
{
|
||||
major_group: "Lower Body",
|
||||
group_name: "Legs",
|
||||
muscles: [
|
||||
{ colloquial_name: "Quads", scientific_name: "Quadriceps femoris" },
|
||||
{ colloquial_name: "Hamstrings", scientific_name: "Hamstring muscles" },
|
||||
{ colloquial_name: "Glutes", scientific_name: "Gluteus maximus" },
|
||||
{ colloquial_name: "Glute Med", scientific_name: "Gluteus medius" },
|
||||
{ colloquial_name: "Glute Min", scientific_name: "Gluteus minimus" },
|
||||
{ colloquial_name: "Adductors", scientific_name: "Adductor muscles" },
|
||||
{ colloquial_name: "Abductors", scientific_name: "Abductor muscles" }
|
||||
]
|
||||
},
|
||||
{
|
||||
major_group: "Lower Body",
|
||||
group_name: "Calves",
|
||||
muscles: [
|
||||
{ colloquial_name: "Gastrocs", scientific_name: "Gastrocnemius" },
|
||||
{ colloquial_name: "Soleus", scientific_name: "Soleus" }
|
||||
]
|
||||
}
|
||||
];
|
||||
|
||||
// Generate SQL INSERT statements
|
||||
function generateSQL(data: MuscleGroup[]): string {
|
||||
let sql = "";
|
||||
|
||||
data.forEach((group) => {
|
||||
group.muscles.forEach((muscle) => {
|
||||
sql += `INSERT INTO muscles (major_group, minor_group, name, scientific_name) VALUES ('${group.major_group}', '${group.group_name}', '${muscle.colloquial_name}', '${muscle.scientific_name}');\n`;
|
||||
});
|
||||
});
|
||||
|
||||
return sql;
|
||||
}
|
||||
|
||||
const sqlStatements = generateSQL(data);
|
||||
console.log(sqlStatements);
|
||||
7
migrations/20241210224609_official_exercises.sql
Normal file
7
migrations/20241210224609_official_exercises.sql
Normal file
@@ -0,0 +1,7 @@
|
||||
-- Add migration script here
|
||||
|
||||
ALTER TABLE exercises
|
||||
ADD COLUMN official BOOLEAN NOT NULL DEFAULT FALSE,
|
||||
ADD COLUMN author_id UUID REFERENCES users(id),
|
||||
ADD COLUMN description TEXT,
|
||||
ADD COLUMN created_at TIMESTAMP NOT NULL DEFAULT NOW();
|
||||
9
migrations/20241211081452_scientific_names.sql
Normal file
9
migrations/20241211081452_scientific_names.sql
Normal file
@@ -0,0 +1,9 @@
|
||||
-- Add migration script here
|
||||
|
||||
ALTER TABLE muscles
|
||||
ADD COLUMN scientific_name VARCHAR(255),
|
||||
ADD COLUMN major_group VARCHAR(255),
|
||||
ADD COLUMN minor_group VARCHAR(255) NOT NULL;
|
||||
|
||||
DROP TABLE muscle_bodypart_relations;
|
||||
DROP TABLE body_parts;
|
||||
1
openapi.json
Normal file
1
openapi.json
Normal file
@@ -0,0 +1 @@
|
||||
{"openapi":"3.1.0","info":{"title":"api","description":"","license":{"name":""},"version":"0.1.0"},"paths":{"/":{"get":{"operationId":"index","responses":{"200":{"description":"Success","content":{"text/plain":{"schema":{"type":"string"}}}}}}},"/.well-known/health-check":{"get":{"summary":"Get health of the API.","operationId":"health_check","responses":{"200":{"description":"Success","content":{"text/plain":{"schema":{"type":"string"}}}}}}},"/api/v1/auth/login":{"post":{"tags":["auth"],"summary":"Login","operationId":"login","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/LoginBody"}}},"required":true},"responses":{"200":{"description":"","content":{"text/plain":{"schema":{"type":"string"}}}}}}},"/api/v1/auth/signup":{"post":{"tags":["auth"],"summary":"Sign up","operationId":"signup","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/SignupBody"}}},"required":true},"responses":{"200":{"description":"","content":{"text/plain":{"schema":{"type":"string"}}}}}}},"/api/v1/exercises/create":{"post":{"tags":["exercises"],"operationId":"create","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateExerciseBody"}}},"required":true},"responses":{"200":{"description":"","content":{"text/plain":{"schema":{"type":"string"}}}}}}},"/api/v1/muscles/all":{"get":{"tags":["muscles"],"operationId":"get_all","responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Muscle"}}}}}}},"/api/v1/muscles/create":{"post":{"tags":["muscles"],"operationId":"create","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateMuscleBody"}}},"required":true},"responses":{"200":{"description":"","content":{"text/plain":{"schema":{"type":"string"}}}}}}}},"components":{"schemas":{"CreateExerciseBody":{"type":"object","required":["name","exercise_type","description","body_parts","primary_muscles","secondary_muscles"],"properties":{"body_parts":{"type":"array","items":{"type":"string"}},"description":{"type":"string"},"exercise_type":{"$ref":"#/components/schemas/ExerciseType"},"name":{"type":"string"},"primary_muscles":{"type":"array","items":{"type":"string"}},"secondary_muscles":{"type":"array","items":{"type":"string"}}}},"CreateMuscleBody":{"type":"object","required":["name","scientific_name","major_group","minor_group"],"properties":{"major_group":{"type":"string"},"minor_group":{"type":"string"},"name":{"type":"string"},"scientific_name":{"type":"string"}}},"ExerciseType":{"type":"string","enum":["Dumbbell","Barbell","Bodyweight","Machine","Kettlebell","ResistanceBand","Cable","MedicineBall","Plyometric","PlateLoadedMachine"]},"LoginBody":{"type":"object","required":["username","password"],"properties":{"password":{"type":"string"},"username":{"type":"string"}}},"Muscle":{"type":"object","required":["id","name","minor_group"],"properties":{"id":{"type":"string"},"major_group":{"type":["string","null"]},"minor_group":{"type":"string"},"name":{"type":"string"},"scientific_name":{"type":["string","null"]}}},"SignupBody":{"type":"object","required":["real_name","username","email","password"],"properties":{"email":{"type":"string"},"password":{"type":"string"},"real_name":{"type":"string"},"username":{"type":"string"}}}}},"tags":[{"name":"auth","description":"Authentication API endpoints"},{"name":"exercises","description":"Exercise API endpoints"},{"name":"muscles","description":"Muscle API endpoints"}]}
|
||||
@@ -11,6 +11,7 @@ pub enum Errors {
|
||||
Ise(anyhow::Error),
|
||||
Unimplemented,
|
||||
Unauthorized,
|
||||
JWTExpired,
|
||||
}
|
||||
|
||||
pub enum AppError {
|
||||
@@ -73,6 +74,12 @@ impl IntoResponse for AppError {
|
||||
Errors::Unimplemented => {
|
||||
(StatusCode::NOT_IMPLEMENTED, "Not implemented").into_response()
|
||||
}
|
||||
|
||||
Errors::JWTExpired => (
|
||||
StatusCode::UNAUTHORIZED,
|
||||
"JWT has expired. Please log in again.",
|
||||
)
|
||||
.into_response(),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
37
src/extractors/jwt.rs
Normal file
37
src/extractors/jwt.rs
Normal file
@@ -0,0 +1,37 @@
|
||||
use crate::*;
|
||||
use axum::{
|
||||
extract::{FromRef, FromRequestParts},
|
||||
http::{header, request::Parts},
|
||||
};
|
||||
|
||||
use jwt::VerifyWithKey;
|
||||
use util::auth::JWTClaims;
|
||||
|
||||
pub struct JWT(JWTClaims);
|
||||
|
||||
impl<S> FromRequestParts<S> for JWT
|
||||
where
|
||||
AppState: FromRef<S>,
|
||||
S: Send + Sync,
|
||||
{
|
||||
type Rejection = AppError;
|
||||
|
||||
async fn from_request_parts(parts: &mut Parts, s: &S) -> Result<Self, Self::Rejection> {
|
||||
let state = AppState::from_ref(s);
|
||||
|
||||
let jwt_string = parts
|
||||
.headers
|
||||
.get(header::AUTHORIZATION)
|
||||
.ok_or(AppError::Error(Errors::Unauthorized))?
|
||||
.to_str()
|
||||
.map_err(|_| AppError::Error(Errors::Unauthorized))?
|
||||
.strip_prefix("Bearer ")
|
||||
.ok_or(AppError::Error(Errors::Unauthorized))?;
|
||||
|
||||
let claims: JWTClaims = jwt_string
|
||||
.verify_with_key(&state.jwt_key)
|
||||
.map_err(|_| AppError::Error(Errors::Unauthorized))?;
|
||||
|
||||
Ok(JWT(claims))
|
||||
}
|
||||
}
|
||||
2
src/extractors/mod.rs
Normal file
2
src/extractors/mod.rs
Normal file
@@ -0,0 +1,2 @@
|
||||
pub mod users;
|
||||
pub mod jwt;
|
||||
44
src/extractors/users.rs
Normal file
44
src/extractors/users.rs
Normal file
@@ -0,0 +1,44 @@
|
||||
use crate::*;
|
||||
use axum::{
|
||||
extract::{FromRef, FromRequestParts},
|
||||
http::{header, request::Parts},
|
||||
};
|
||||
|
||||
use jwt::VerifyWithKey;
|
||||
use sqlx::types::Uuid;
|
||||
use util::auth::JWTClaims;
|
||||
|
||||
/// Remember, this is FromRequestParts, so it has to be ABOVE the extractors
|
||||
/// that eat the entire request
|
||||
pub struct UserId(pub Uuid);
|
||||
|
||||
impl<S> FromRequestParts<S> for UserId
|
||||
where
|
||||
AppState: FromRef<S>,
|
||||
S: Send + Sync,
|
||||
{
|
||||
type Rejection = AppError;
|
||||
|
||||
async fn from_request_parts(parts: &mut Parts, s: &S) -> Result<Self, Self::Rejection> {
|
||||
let state = AppState::from_ref(s);
|
||||
|
||||
let jwt_string = parts
|
||||
.headers
|
||||
.get(header::AUTHORIZATION)
|
||||
.ok_or(AppError::Error(Errors::Unauthorized))?
|
||||
.to_str()
|
||||
.map_err(|_| AppError::Error(Errors::Unauthorized))?
|
||||
.strip_prefix("Bearer ")
|
||||
.ok_or(AppError::Error(Errors::Unauthorized))?;
|
||||
|
||||
let claims: JWTClaims = jwt_string
|
||||
.verify_with_key(&state.jwt_key)
|
||||
.map_err(|_| AppError::Error(Errors::Unauthorized))?;
|
||||
|
||||
if claims.exp < chrono::Utc::now().timestamp() {
|
||||
return Err(AppError::Error(Errors::JWTExpired));
|
||||
}
|
||||
|
||||
Ok(UserId(claims.sub))
|
||||
}
|
||||
}
|
||||
28
src/main.rs
28
src/main.rs
@@ -1,17 +1,22 @@
|
||||
use std::{net::Ipv4Addr, sync::Arc};
|
||||
use axum::http::Method;
|
||||
use hmac::{Hmac, Mac};
|
||||
use tokio::net::TcpListener;
|
||||
use tower_http::cors::{Any, CorsLayer};
|
||||
use utoipa::OpenApi;
|
||||
use utoipa_axum::{router::OpenApiRouter, routes};
|
||||
use utoipa_swagger_ui::SwaggerUi;
|
||||
|
||||
// tags
|
||||
use v1::auth::AUTH_TAG;
|
||||
use v1::{auth::AUTH_TAG, exercises::EXERCISES_TAG, muscles::MUSCLES_TAG};
|
||||
|
||||
mod v1;
|
||||
mod structs;
|
||||
mod state;
|
||||
mod db;
|
||||
mod error;
|
||||
mod util;
|
||||
mod extractors;
|
||||
|
||||
pub(crate) use anyhow::Context;
|
||||
pub(crate) use axum::extract::{Json, State};
|
||||
@@ -24,6 +29,8 @@ pub(crate) use utoipa::ToSchema;
|
||||
#[openapi(
|
||||
tags(
|
||||
(name = AUTH_TAG, description = "Authentication API endpoints"),
|
||||
(name = EXERCISES_TAG, description = "Exercise API endpoints"),
|
||||
(name = MUSCLES_TAG, description = "Muscle API endpoints"),
|
||||
// (name = CUSTOMER_TAG, description = "Customer API endpoints"),
|
||||
// (name = ORDER_TAG, description = "Order API endpoints")
|
||||
),
|
||||
@@ -56,18 +63,29 @@ async fn index() -> &'static str {
|
||||
#[tokio::main]
|
||||
async fn main() -> anyhow::Result<()> {
|
||||
dotenv::dotenv().ok();
|
||||
let db = db::db().await?;
|
||||
let state = state::AppState { db: Arc::new(db) };
|
||||
|
||||
let port: u16 = std::env::var("PORT").unwrap_or_else(|_| "8080".to_string()).parse()?;
|
||||
let jwt_secret = std::env::var("JWT_SECRET").unwrap_or_else(|_| "secret".to_string());
|
||||
|
||||
let db = db::db().await?;
|
||||
let state = state::AppState { db: Arc::new(db),
|
||||
jwt_key: Hmac::new_from_slice(jwt_secret.as_bytes()).context("Failed to create HMAC")?
|
||||
};
|
||||
|
||||
let cors = CorsLayer::new()
|
||||
.allow_methods(Any)
|
||||
.allow_origin(["http://localhost:5173".parse().unwrap()])
|
||||
.allow_headers(Any);
|
||||
|
||||
let (router, api) = OpenApiRouter::with_openapi(ApiDoc::openapi())
|
||||
.routes(routes!(health_check))
|
||||
.routes(routes!(index))
|
||||
.with_state(state.clone())
|
||||
.nest("/api/v1/auth", v1::auth::router(state.clone()))
|
||||
.nest("/api/v1", v1::router(state.clone()))
|
||||
.layer(cors)
|
||||
.split_for_parts();
|
||||
|
||||
tokio::fs::write("openapi.json", api.to_json()?).await?;
|
||||
|
||||
let router = router.merge(SwaggerUi::new("/docs").url("/docs/openapi.json", api));
|
||||
|
||||
println!("Listening on http://localhost:{port}");
|
||||
|
||||
@@ -1,8 +1,13 @@
|
||||
use std::sync::Arc;
|
||||
|
||||
use hmac::Hmac;
|
||||
use sha2::Sha256;
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct AppState {
|
||||
pub db: DB,
|
||||
// pub jwt_secret: String,
|
||||
pub jwt_key: Hmac<Sha256>,
|
||||
}
|
||||
|
||||
pub type DB = Arc<sqlx::Pool<sqlx::Postgres>>;
|
||||
|
||||
55
src/structs/exercise_types.rs
Normal file
55
src/structs/exercise_types.rs
Normal file
@@ -0,0 +1,55 @@
|
||||
use anyhow::anyhow;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use utoipa::ToSchema;
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, PartialOrd, sqlx::Type, Deserialize, Serialize, ToSchema)]
|
||||
#[sqlx(type_name = "exercise_type", rename_all = "snake_case")]
|
||||
pub enum ExerciseType {
|
||||
Dumbbell,
|
||||
Barbell,
|
||||
Bodyweight,
|
||||
Machine,
|
||||
Kettlebell,
|
||||
ResistanceBand,
|
||||
Cable,
|
||||
MedicineBall,
|
||||
Plyometric,
|
||||
PlateLoadedMachine,
|
||||
}
|
||||
|
||||
impl std::fmt::Display for ExerciseType {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
match self {
|
||||
ExerciseType::Dumbbell => write!(f, "dumbbell"),
|
||||
ExerciseType::Barbell => write!(f, "barbell"),
|
||||
ExerciseType::Bodyweight => write!(f, "bodyweight"),
|
||||
ExerciseType::Machine => write!(f, "machine"),
|
||||
ExerciseType::Kettlebell => write!(f, "kettlebell"),
|
||||
ExerciseType::ResistanceBand => write!(f, "resistance_band"),
|
||||
ExerciseType::Cable => write!(f, "cable"),
|
||||
ExerciseType::MedicineBall => write!(f, "medicine_ball"),
|
||||
ExerciseType::Plyometric => write!(f, "plyometric"),
|
||||
ExerciseType::PlateLoadedMachine => write!(f, "plate_loaded_machine"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl std::str::FromStr for ExerciseType {
|
||||
type Err = anyhow::Error;
|
||||
|
||||
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||||
match s {
|
||||
"dumbbell" => Ok(ExerciseType::Dumbbell),
|
||||
"barbell" => Ok(ExerciseType::Barbell),
|
||||
"bodyweight" => Ok(ExerciseType::Bodyweight),
|
||||
"machine" => Ok(ExerciseType::Machine),
|
||||
"kettlebell" => Ok(ExerciseType::Kettlebell),
|
||||
"resistance_band" => Ok(ExerciseType::ResistanceBand),
|
||||
"cable" => Ok(ExerciseType::Cable),
|
||||
"medicine_ball" => Ok(ExerciseType::MedicineBall),
|
||||
"plyometric" => Ok(ExerciseType::Plyometric),
|
||||
"plate_loaded_machine" => Ok(ExerciseType::PlateLoadedMachine),
|
||||
_ => Err(anyhow!("Invalid exercise type")),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1 +1,2 @@
|
||||
pub mod users;
|
||||
pub mod exercise_types;
|
||||
|
||||
49
src/util/auth.rs
Normal file
49
src/util/auth.rs
Normal file
@@ -0,0 +1,49 @@
|
||||
use chrono::Utc;
|
||||
use uuid::Uuid;
|
||||
|
||||
use crate::*;
|
||||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
pub struct JWTClaims {
|
||||
pub sub: Uuid,
|
||||
pub iat: i64,
|
||||
pub exp: i64,
|
||||
|
||||
pub username: String,
|
||||
pub real_name: String,
|
||||
pub email: String,
|
||||
}
|
||||
|
||||
impl JWTClaims {
|
||||
pub fn new(sub: Uuid, username: String, real_name: String, email: String) -> Self {
|
||||
let iat = Utc::now().timestamp();
|
||||
let exp = iat + 60 * 60 * 24 * 7;
|
||||
Self {
|
||||
sub,
|
||||
iat,
|
||||
exp,
|
||||
username,
|
||||
real_name,
|
||||
email,
|
||||
}
|
||||
}
|
||||
|
||||
/// Create a new JWT Claims with a custom expiration time. Expiration time is added to current time.
|
||||
pub fn new_with_exp(
|
||||
sub: Uuid,
|
||||
username: String,
|
||||
real_name: String,
|
||||
email: String,
|
||||
exp: i64,
|
||||
) -> Self {
|
||||
let iat = Utc::now().timestamp();
|
||||
Self {
|
||||
sub,
|
||||
iat,
|
||||
exp: iat + exp,
|
||||
username,
|
||||
real_name,
|
||||
email,
|
||||
}
|
||||
}
|
||||
}
|
||||
1
src/util/mod.rs
Normal file
1
src/util/mod.rs
Normal file
@@ -0,0 +1 @@
|
||||
pub mod auth;
|
||||
48
src/v1/auth/login.rs
Normal file
48
src/v1/auth/login.rs
Normal file
@@ -0,0 +1,48 @@
|
||||
use crate::*;
|
||||
use argon2::{
|
||||
password_hash::{PasswordHash, PasswordVerifier},
|
||||
Argon2,
|
||||
};
|
||||
use jwt::SignWithKey;
|
||||
use sqlx::query;
|
||||
use util::auth::JWTClaims;
|
||||
|
||||
#[derive(Serialize, Deserialize, ToSchema)]
|
||||
pub struct LoginBody {
|
||||
username: String,
|
||||
password: String,
|
||||
}
|
||||
|
||||
/// Login
|
||||
#[utoipa::path(post, path = "/login", responses((status = OK, body = String)), tag = super::AUTH_TAG)]
|
||||
pub async fn login(
|
||||
State(state): State<AppState>,
|
||||
Json(body): Json<LoginBody>,
|
||||
) -> Result<String, AppError> {
|
||||
let user = query!(
|
||||
"SELECT id, real_name, username, email, password_hash, is_admin, created_at
|
||||
FROM users
|
||||
WHERE username = $1",
|
||||
body.username,
|
||||
)
|
||||
.fetch_one(&*state.db)
|
||||
.await?;
|
||||
|
||||
let argon2 = Argon2::default();
|
||||
let hash = PasswordHash::new(&user.password_hash).expect("Password hashing failed");
|
||||
|
||||
if !argon2
|
||||
.verify_password(body.password.as_bytes(), &hash)
|
||||
.is_ok()
|
||||
{
|
||||
return Err(AppError::Error(Errors::Unauthorized));
|
||||
}
|
||||
|
||||
let claims = JWTClaims::new(user.id, user.username, user.real_name, user.email);
|
||||
|
||||
let token_str = claims
|
||||
.sign_with_key(&state.jwt_key)
|
||||
.context("Failed to sign JWT")?;
|
||||
|
||||
Ok(token_str)
|
||||
}
|
||||
@@ -1,13 +1,15 @@
|
||||
use crate::state::AppState;
|
||||
use crate::AppState;
|
||||
|
||||
pub(super) use super::*;
|
||||
|
||||
pub mod signup;
|
||||
pub mod login;
|
||||
|
||||
pub const AUTH_TAG: &str = "auth";
|
||||
|
||||
pub fn router(state: AppState) -> OpenApiRouter {
|
||||
pub(super) fn router(state: AppState) -> OpenApiRouter {
|
||||
OpenApiRouter::new()
|
||||
.routes(routes!(signup::signup))
|
||||
.routes(routes!(login::login))
|
||||
.with_state(state)
|
||||
}
|
||||
|
||||
@@ -1,6 +1,14 @@
|
||||
use crate::*;
|
||||
use sqlx::query;
|
||||
|
||||
use argon2::{
|
||||
password_hash::{rand_core::OsRng, PasswordHasher, SaltString},
|
||||
Argon2,
|
||||
};
|
||||
|
||||
use jwt::SignWithKey;
|
||||
use util::auth::JWTClaims;
|
||||
|
||||
#[derive(Serialize, Deserialize, ToSchema)]
|
||||
pub struct SignupBody {
|
||||
real_name: String,
|
||||
@@ -10,11 +18,19 @@ pub struct SignupBody {
|
||||
}
|
||||
|
||||
/// Sign up
|
||||
#[utoipa::path(get, path = "/signup", responses((status = OK, body = String)), tag = super::AUTH_TAG)]
|
||||
#[utoipa::path(post, path = "/signup", responses((status = OK, body = String)), tag = super::AUTH_TAG)]
|
||||
pub async fn signup(
|
||||
State(state): State<AppState>,
|
||||
Json(body): Json<SignupBody>,
|
||||
) -> Result<String, AppError> {
|
||||
let salt = SaltString::generate(&mut OsRng);
|
||||
let argon2 = Argon2::default();
|
||||
|
||||
let hash = argon2
|
||||
.hash_password(body.password.as_bytes(), &salt)
|
||||
.expect("Password hashing failed")
|
||||
.to_string();
|
||||
|
||||
let user = query!(
|
||||
"INSERT INTO users (real_name, username, email, password_hash)
|
||||
VALUES ($1, $2, $3, $4)
|
||||
@@ -22,10 +38,16 @@ pub async fn signup(
|
||||
body.real_name,
|
||||
body.username,
|
||||
body.email,
|
||||
body.password,
|
||||
hash
|
||||
)
|
||||
.fetch_one(&*state.db)
|
||||
.await?;
|
||||
|
||||
Ok(user.id.to_string())
|
||||
let claims = JWTClaims::new(user.id, user.username, user.real_name, user.email);
|
||||
|
||||
let token_str = claims
|
||||
.sign_with_key(&state.jwt_key)
|
||||
.context("Failed to sign JWT")?;
|
||||
|
||||
Ok(token_str)
|
||||
}
|
||||
|
||||
106
src/v1/exercises/create.rs
Normal file
106
src/v1/exercises/create.rs
Normal file
@@ -0,0 +1,106 @@
|
||||
use chrono::NaiveDateTime;
|
||||
use extractors::users::UserId;
|
||||
use sqlx::query;
|
||||
use structs::exercise_types::ExerciseType;
|
||||
use uuid::Uuid;
|
||||
|
||||
use crate::*;
|
||||
|
||||
#[derive(Serialize, Deserialize, ToSchema)]
|
||||
pub struct CreateExerciseBody {
|
||||
name: String,
|
||||
// todo: make this an enum
|
||||
exercise_type: ExerciseType,
|
||||
description: String,
|
||||
body_parts: Vec<String>,
|
||||
primary_muscles: Vec<String>,
|
||||
secondary_muscles: Vec<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize, Serialize, Clone, sqlx::Type, PartialEq, sqlx::FromRow)]
|
||||
pub struct Exercise {
|
||||
pub id: Uuid,
|
||||
pub name: String,
|
||||
pub exercise_type: ExerciseType,
|
||||
pub description: String,
|
||||
pub author_id: Uuid,
|
||||
pub official: bool,
|
||||
pub created_at: NaiveDateTime,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize, Serialize)]
|
||||
pub struct Muscle {
|
||||
pub id: Uuid,
|
||||
pub is_primary: bool,
|
||||
}
|
||||
|
||||
#[utoipa::path(post, path = "/create", responses((status = OK, body = String)), tag = super::EXERCISES_TAG)]
|
||||
pub async fn create(
|
||||
State(state): State<AppState>,
|
||||
UserId(user): UserId,
|
||||
Json(body): Json<CreateExerciseBody>,
|
||||
) -> Result<String, AppError> {
|
||||
let is_admin = query!("SELECT is_admin FROM users WHERE id = $1", user)
|
||||
.fetch_one(&*state.db)
|
||||
.await?
|
||||
.is_admin;
|
||||
|
||||
let muscles: Vec<Muscle> = body
|
||||
.primary_muscles
|
||||
.iter()
|
||||
.map(|id| -> anyhow::Result<Muscle> {
|
||||
let uuid = Uuid::parse_str(id)?;
|
||||
Ok(Muscle {
|
||||
id: uuid,
|
||||
is_primary: true,
|
||||
})
|
||||
})
|
||||
.chain(
|
||||
body.secondary_muscles
|
||||
.iter()
|
||||
.map(|id| -> anyhow::Result<Muscle> {
|
||||
let uuid = Uuid::parse_str(id)?;
|
||||
Ok(Muscle {
|
||||
id: uuid,
|
||||
is_primary: false,
|
||||
})
|
||||
}),
|
||||
)
|
||||
.collect::<Result<Vec<Muscle>, _>>()
|
||||
.context("Failed to parse muscle ids")?;
|
||||
|
||||
let mut tx = state.db.begin().await?;
|
||||
|
||||
let exercise = query!(
|
||||
r#"
|
||||
INSERT INTO exercises (name, exercise_type, official, author_id, description)
|
||||
VALUES ($1, $2, $3, $4, $5)
|
||||
RETURNING id
|
||||
"#,
|
||||
body.name,
|
||||
body.exercise_type as ExerciseType,
|
||||
is_admin,
|
||||
user,
|
||||
body.description
|
||||
)
|
||||
.fetch_one(&mut *tx)
|
||||
// .fetch_one(&*state.db)
|
||||
.await?;
|
||||
|
||||
query!(
|
||||
r#"
|
||||
INSERT INTO exercise_muscle_relations (exercise_id, muscle_id, is_primary)
|
||||
SELECT * FROM UNNEST($1::uuid[], $2::uuid[], $3::boolean[])
|
||||
"#,
|
||||
&vec![exercise.id; muscles.len()],
|
||||
&muscles.iter().map(|m| m.id).collect::<Vec<Uuid>>(),
|
||||
&muscles.iter().map(|m| m.is_primary).collect::<Vec<bool>>()
|
||||
)
|
||||
.execute(&mut *tx)
|
||||
// .execute(&*state.db)
|
||||
.await?;
|
||||
|
||||
tx.commit().await?;
|
||||
|
||||
Ok(exercise.id.to_string())
|
||||
}
|
||||
13
src/v1/exercises/mod.rs
Normal file
13
src/v1/exercises/mod.rs
Normal file
@@ -0,0 +1,13 @@
|
||||
use crate::AppState;
|
||||
|
||||
pub(super) use super::*;
|
||||
|
||||
pub mod create;
|
||||
|
||||
pub const EXERCISES_TAG: &str = "exercises";
|
||||
|
||||
pub(super) fn router(state: AppState) -> OpenApiRouter {
|
||||
OpenApiRouter::new()
|
||||
.routes(routes!(create::create))
|
||||
.with_state(state)
|
||||
}
|
||||
@@ -1,4 +1,16 @@
|
||||
pub(super) use utoipa_axum::router::OpenApiRouter;
|
||||
pub(super) use utoipa_axum::routes;
|
||||
|
||||
use crate::AppState;
|
||||
|
||||
pub mod auth;
|
||||
pub mod exercises;
|
||||
pub mod muscles;
|
||||
|
||||
pub fn router(state: AppState) -> OpenApiRouter {
|
||||
OpenApiRouter::new()
|
||||
.with_state(state.clone())
|
||||
.nest("/auth", auth::router(state.clone()))
|
||||
.nest("/exercises", exercises::router(state.clone()))
|
||||
.nest("/muscles", muscles::router(state.clone()))
|
||||
}
|
||||
|
||||
40
src/v1/muscles/create.rs
Normal file
40
src/v1/muscles/create.rs
Normal file
@@ -0,0 +1,40 @@
|
||||
use extractors::users::UserId;
|
||||
use sqlx::query;
|
||||
|
||||
use crate::*;
|
||||
|
||||
#[derive(Serialize, Deserialize, ToSchema)]
|
||||
pub struct CreateMuscleBody {
|
||||
name: String,
|
||||
scientific_name: String,
|
||||
major_group: String,
|
||||
minor_group: String,
|
||||
}
|
||||
|
||||
#[utoipa::path(post, path = "/create", responses((status = OK, body = String)), tag = super::MUSCLES_TAG)]
|
||||
pub async fn create(
|
||||
State(state): State<AppState>,
|
||||
UserId(user_id): UserId,
|
||||
Json(body): Json<CreateMuscleBody>,
|
||||
) -> Result<String, AppError> {
|
||||
let is_admin = query!("SELECT is_admin FROM users WHERE id = $1", user_id)
|
||||
.fetch_one(&*state.db)
|
||||
.await?
|
||||
.is_admin;
|
||||
|
||||
if !is_admin {
|
||||
return Err(AppError::Error(Errors::Unauthorized));
|
||||
}
|
||||
|
||||
let out = query!(
|
||||
"INSERT INTO muscles (name, scientific_name, major_group, minor_group) VALUES ($1, $2, $3, $4) RETURNING id",
|
||||
body.name,
|
||||
body.scientific_name,
|
||||
body.major_group,
|
||||
body.minor_group,
|
||||
)
|
||||
.fetch_one(&*state.db)
|
||||
.await?;
|
||||
|
||||
Ok(out.id.to_string())
|
||||
}
|
||||
23
src/v1/muscles/get_all.rs
Normal file
23
src/v1/muscles/get_all.rs
Normal file
@@ -0,0 +1,23 @@
|
||||
// use extractors::users::UserId;
|
||||
use sqlx::query_as;
|
||||
|
||||
use crate::*;
|
||||
|
||||
#[derive(Serialize, Deserialize, ToSchema)]
|
||||
pub struct Muscle {
|
||||
id: String,
|
||||
name: String,
|
||||
scientific_name: Option<String>,
|
||||
major_group: Option<String>,
|
||||
minor_group: String,
|
||||
}
|
||||
|
||||
#[utoipa::path(get, path = "/all", responses((status = OK, body = Muscle)), tag = super::MUSCLES_TAG)]
|
||||
pub async fn get_all(State(state): State<AppState>) -> Result<Json<Vec<Muscle>>, AppError> {
|
||||
let muscles = query_as!(Muscle, "SELECT * FROM muscles")
|
||||
.fetch_all(&*state.db)
|
||||
.await?;
|
||||
|
||||
Ok(Json(muscles))
|
||||
}
|
||||
|
||||
15
src/v1/muscles/mod.rs
Normal file
15
src/v1/muscles/mod.rs
Normal file
@@ -0,0 +1,15 @@
|
||||
use crate::AppState;
|
||||
|
||||
pub(super) use super::*;
|
||||
|
||||
pub mod create;
|
||||
pub mod get_all;
|
||||
|
||||
pub const MUSCLES_TAG: &str = "muscles";
|
||||
|
||||
pub(super) fn router(state: AppState) -> OpenApiRouter {
|
||||
OpenApiRouter::new()
|
||||
.routes(routes!(create::create))
|
||||
.routes(routes!(get_all::get_all))
|
||||
.with_state(state)
|
||||
}
|
||||
Reference in New Issue
Block a user