diff --git a/Cargo.lock b/Cargo.lock index 69deaa2..53894ae 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -26,6 +26,12 @@ dependencies = [ "memchr", ] +[[package]] +name = "aligned-vec" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4aa90d7ce82d4be67b64039a3d588d38dbcc6736577de4a847025ce5b0c468d1" + [[package]] name = "android-tzdata" version = "0.1.1" @@ -41,6 +47,29 @@ dependencies = [ "libc", ] +[[package]] +name = "anyhow" +version = "1.0.81" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0952808a6c2afd1aa8947271f3a60f1a6763c7b912d210184c5149b5cf147247" + +[[package]] +name = "arbitrary" +version = "1.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7d5a26814d8dcb93b0e5a0ff3c6d80a8843bafb21b39e8e18a6f05471870e110" + +[[package]] +name = "arg_enum_proc_macro" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ae92a5119aa49cdbcf6b9f893fe4e1d98b04ccbf82ee0584ad948a44a734dea" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.52", +] + [[package]] name = "argon2" version = "0.5.3" @@ -53,6 +82,12 @@ dependencies = [ "password-hash", ] +[[package]] +name = "arrayvec" +version = "0.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96d30a06541fbafbc7f82ed10c06164cfbd2c401138f6addd8404629c4b16711" + [[package]] name = "askama" version = "0.12.1" @@ -90,7 +125,7 @@ dependencies = [ "proc-macro2", "quote", "serde", - "syn", + "syn 2.0.52", ] [[package]] @@ -108,6 +143,19 @@ dependencies = [ "nom", ] +[[package]] +name = "async-compression" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a116f46a969224200a0a97f29cfd4c50e7534e4b4826bd23ea2c3c533039c82c" +dependencies = [ + "flate2", + "futures-core", + "memchr", + "pin-project-lite", + "tokio", +] + [[package]] name = "async-trait" version = "0.1.77" @@ -116,7 +164,7 @@ checksum = "c980ee35e870bd1a4d2c8294d4c04d0499e67bca1e4b5cefcc693c2fa00caea9" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.52", ] [[package]] @@ -125,6 +173,29 @@ version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa" +[[package]] +name = "av1-grain" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6678909d8c5d46a42abcf571271e15fdbc0a225e3646cf23762cd415046c78bf" +dependencies = [ + "anyhow", + "arrayvec", + "log", + "nom", + "num-rational", + "v_frame", +] + +[[package]] +name = "avif-serialize" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "876c75a42f6364451a033496a14c44bffe41f5f4a8236f697391f11024e596d2" +dependencies = [ + "arrayvec", +] + [[package]] name = "axum" version = "0.7.4" @@ -133,6 +204,7 @@ checksum = "1236b4b292f6c4d6dc34604bb5120d85c3fe1d1aa596bd5cc52ca054d13e7b9e" dependencies = [ "async-trait", "axum-core", + "axum-macros", "bytes", "futures-util", "http", @@ -144,6 +216,7 @@ dependencies = [ "matchit", "memchr", "mime", + "multer", "percent-encoding", "pin-project-lite", "rustversion", @@ -202,6 +275,18 @@ dependencies = [ "tower-service", ] +[[package]] +name = "axum-macros" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "00c055ee2d014ae5981ce1016374e8213682aa14d9bf40e48ab48b5f3ef20eaa" +dependencies = [ + "heck 0.4.1", + "proc-macro2", + "quote", + "syn 2.0.52", +] + [[package]] name = "backtrace" version = "0.3.69" @@ -238,12 +323,30 @@ dependencies = [ "serde", ] +[[package]] +name = "bit_field" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc827186963e592360843fb5ba4b973e145841266c1357f7180c43526f2e5b61" + +[[package]] +name = "bitflags" +version = "1.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" + [[package]] name = "bitflags" version = "2.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ed570934406eb16438a4e976b1b4500774099c13b8cb96eec99f620f05090ddf" +[[package]] +name = "bitstream-io" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06c9989a51171e2e81038ab168b6ae22886fe9ded214430dbb4f41c28cf176da" + [[package]] name = "blake2" version = "0.10.6" @@ -272,12 +375,24 @@ dependencies = [ "serde", ] +[[package]] +name = "built" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38d17f4d6e4dc36d1a02fbedc2753a096848e7c1b0772f7654eab8e2c927dd53" + [[package]] name = "bumpalo" version = "3.15.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7ff69b9dd49fd426c69a0db9fc04dd934cdb6645ff000864d98f7e2af8830eaa" +[[package]] +name = "bytemuck" +version = "1.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d6d68c57235a3a081186990eca2867354726650f42f7516ca50c28d6281fd15" + [[package]] name = "byteorder" version = "1.5.0" @@ -295,6 +410,20 @@ name = "cc" version = "1.0.90" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8cd6604a82acf3039f1144f54b8eb34e91ffba622051189e71b781822d5ee1f5" +dependencies = [ + "jobserver", + "libc", +] + +[[package]] +name = "cfg-expr" +version = "0.15.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fa50868b64a9a6fda9d593ce778849ea8715cd2a3d2cc17ffdb4a2f2f2f1961d" +dependencies = [ + "smallvec", + "target-lexicon", +] [[package]] name = "cfg-if" @@ -317,6 +446,12 @@ dependencies = [ "windows-targets 0.52.4", ] +[[package]] +name = "color_quant" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d7b894f5411737b7867f4827955924d7c254fc9f4d91a6aad6b097804b1018b" + [[package]] name = "cookie" version = "0.18.0" @@ -343,6 +478,15 @@ dependencies = [ "libc", ] +[[package]] +name = "crc32fast" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b3855a8a784b474f333699ef2bbca9db2c4a1f6d9088a90a2d25b1eb53111eaa" +dependencies = [ + "cfg-if", +] + [[package]] name = "crossbeam-deque" version = "0.8.5" @@ -368,6 +512,12 @@ version = "0.8.19" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "248e3bacc7dc6baa3b21e405ee045c3047101a49145e7e9eca583ab4c2ca5345" +[[package]] +name = "crunchy" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a81dae078cea95a014a339291cec439d2f232ebe854a9d672b796c6afafa9b7" + [[package]] name = "crypto-common" version = "0.1.6" @@ -378,6 +528,41 @@ dependencies = [ "typenum", ] +[[package]] +name = "darling" +version = "0.20.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "54e36fcd13ed84ffdfda6f5be89b31287cbb80c439841fe69e04841435464391" +dependencies = [ + "darling_core", + "darling_macro", +] + +[[package]] +name = "darling_core" +version = "0.20.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c2cf1c23a687a1feeb728783b993c4e1ad83d99f351801977dd809b48d0a70f" +dependencies = [ + "fnv", + "ident_case", + "proc-macro2", + "quote", + "strsim", + "syn 2.0.52", +] + +[[package]] +name = "darling_macro" +version = "0.20.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a668eda54683121533a393014d8692171709ff57a7d61f187b6e782719f8933f" +dependencies = [ + "darling_core", + "quote", + "syn 2.0.52", +] + [[package]] name = "deadpool" version = "0.10.0" @@ -390,13 +575,24 @@ dependencies = [ "tokio", ] +[[package]] +name = "deadpool" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "144f5e4b9ce67c972acc225e71aefe6b21241276f94005024562874611064d30" +dependencies = [ + "deadpool-runtime", + "num_cpus", + "tokio", +] + [[package]] name = "deadpool-diesel" version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bfa8404d25ddc6cb0676d4a863bbd007613ee3fffb54db23e0e6341e1fe61c3e" dependencies = [ - "deadpool", + "deadpool 0.10.0", "deadpool-sync", "diesel", ] @@ -428,13 +624,44 @@ dependencies = [ "powerfmt", ] +[[package]] +name = "derive_builder" +version = "0.20.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0350b5cb0331628a5916d6c5c0b72e97393b8b6b03b47a9284f4e7f5a405ffd7" +dependencies = [ + "derive_builder_macro", +] + +[[package]] +name = "derive_builder_core" +version = "0.20.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d48cda787f839151732d396ac69e3473923d54312c070ee21e9effcaa8ca0b1d" +dependencies = [ + "darling", + "proc-macro2", + "quote", + "syn 2.0.52", +] + +[[package]] +name = "derive_builder_macro" +version = "0.20.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "206868b8242f27cecce124c19fd88157fbd0dd334df2587f36417bafbc85097b" +dependencies = [ + "derive_builder_core", + "syn 2.0.52", +] + [[package]] name = "diesel" version = "2.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "62c6fcf842f17f8c78ecf7c81d75c5ce84436b41ee07e03f490fbb5f5a8731d8" dependencies = [ - "bitflags", + "bitflags 2.4.2", "byteorder", "chrono", "diesel_derives", @@ -453,7 +680,7 @@ dependencies = [ "diesel_table_macro_syntax", "proc-macro2", "quote", - "syn", + "syn 2.0.52", ] [[package]] @@ -473,7 +700,7 @@ version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fc5557efc453706fed5e4fa85006fe9817c224c3f480a34c7e5959fd700921c5" dependencies = [ - "syn", + "syn 2.0.52", ] [[package]] @@ -494,37 +721,115 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1aaf95b3e5c8f23aa320147307562d361db0ae0d51242340f558153b4eb2439b" [[package]] -name = "elnafo" +name = "either" +version = "1.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "11157ac094ffbdde99aa67b23417ebdd801842852b500e395a45a9c0aac03e4a" + +[[package]] +name = "elnafo-backend" version = "0.1.0" dependencies = [ "argon2", "axum", "axum-extra", "chrono", + "deadpool 0.11.1", "deadpool-diesel", + "deadpool-sync", "diesel", "diesel_migrations", "dotenvy", - "frontend", + "elnafo-frontend", + "glob", + "image", "jsonwebtoken", "mime_guess", "rand_core", "serde", "serde_json", + "sqids", "time", "tokio", + "toml 0.8.12", "tower-http", "tracing", "tracing-subscriber", + "utoipa", + "utoipa-rapidoc", "uuid", ] +[[package]] +name = "elnafo-frontend" +version = "0.1.0" +dependencies = [ + "askama", + "askama_axum", + "ignore", + "npm_rs", + "rust-embed", +] + +[[package]] +name = "encoding_rs" +version = "0.8.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7268b386296a025e474d5140678f75d6de9493ae55a5d709eeb9dd08149945e1" +dependencies = [ + "cfg-if", +] + [[package]] name = "equivalent" version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5" +[[package]] +name = "exr" +version = "1.72.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "887d93f60543e9a9362ef8a21beedd0a833c5d9610e18c67abe15a5963dcb1a4" +dependencies = [ + "bit_field", + "flume", + "half", + "lebe", + "miniz_oxide", + "rayon-core", + "smallvec", + "zune-inflate", +] + +[[package]] +name = "fdeflate" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4f9bfee30e4dedf0ab8b422f03af778d9612b63f502710fc500a334ebe2de645" +dependencies = [ + "simd-adler32", +] + +[[package]] +name = "flate2" +version = "1.0.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "46303f565772937ffe1d394a4fac6f411c6013172fadde9dcdb1e147a086940e" +dependencies = [ + "crc32fast", + "miniz_oxide", +] + +[[package]] +name = "flume" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "55ac459de2512911e4b674ce33cf20befaba382d05b62b008afc1c8b57cbf181" +dependencies = [ + "spin", +] + [[package]] name = "fnv" version = "1.0.7" @@ -540,17 +845,6 @@ dependencies = [ "percent-encoding", ] -[[package]] -name = "frontend" -version = "0.1.0" -dependencies = [ - "askama", - "askama_axum", - "ignore", - "npm_rs", - "rust-embed", -] - [[package]] name = "futures-channel" version = "0.3.30" @@ -613,12 +907,28 @@ dependencies = [ "wasm-bindgen", ] +[[package]] +name = "gif" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3fb2d69b19215e18bb912fa30f7ce15846e301408695e44e0ef719f1da9e19f2" +dependencies = [ + "color_quant", + "weezl", +] + [[package]] name = "gimli" version = "0.28.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4271d37baee1b8c7e4b708028c57d816cf9d2434acb33a549475f78c181f6253" +[[package]] +name = "glob" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d2fabcfbdc87f4758337ca535fb41a6d701b65693ce38287d856d1674551ec9b" + [[package]] name = "globset" version = "0.4.14" @@ -651,12 +961,34 @@ dependencies = [ "tracing", ] +[[package]] +name = "half" +version = "2.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b5eceaaeec696539ddaf7b333340f1af35a5aa87ae3e4f3ead0532f72affab2e" +dependencies = [ + "cfg-if", + "crunchy", +] + [[package]] name = "hashbrown" version = "0.14.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "290f1a1d9242c78d09ce40a5e87e7554ee637af1351968159f4952f028f75604" +[[package]] +name = "heck" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8" + +[[package]] +name = "heck" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" + [[package]] name = "hermit-abi" version = "0.3.9" @@ -777,6 +1109,12 @@ dependencies = [ "cc", ] +[[package]] +name = "ident_case" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39" + [[package]] name = "ignore" version = "0.4.22" @@ -793,6 +1131,45 @@ dependencies = [ "winapi-util", ] +[[package]] +name = "image" +version = "0.25.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fd54d660e773627692c524beaad361aca785a4f9f5730ce91f42aabe5bce3d11" +dependencies = [ + "bytemuck", + "byteorder", + "color_quant", + "exr", + "gif", + "image-webp", + "num-traits", + "png", + "qoi", + "ravif", + "rayon", + "rgb", + "tiff", + "zune-core", + "zune-jpeg", +] + +[[package]] +name = "image-webp" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a84a25dcae3ac487bc24ef280f9e20c79c9b1a3e5e32cbed3041d1c514aa87c" +dependencies = [ + "byteorder", + "thiserror", +] + +[[package]] +name = "imgref" +version = "1.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "44feda355f4159a7c757171a77de25daf6411e217b4cabd03bd6650690468126" + [[package]] name = "indexmap" version = "2.2.5" @@ -801,6 +1178,27 @@ checksum = "7b0b929d511467233429c45a44ac1dcaa21ba0f5ba11e4879e6ed28ddb4f9df4" dependencies = [ "equivalent", "hashbrown", + "serde", +] + +[[package]] +name = "interpolate_name" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c34819042dc3d3971c46c2190835914dfbe0c3c13f61449b2997f4e9722dfa60" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.52", +] + +[[package]] +name = "itertools" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba291022dbbd398a455acf126c1e341954079855bc60dfdda641363bd6922569" +dependencies = [ + "either", ] [[package]] @@ -809,6 +1207,21 @@ version = "1.0.10" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b1a46d1a171d865aa5f83f92695765caa047a9b4cbae2cbf37dbd613a793fd4c" +[[package]] +name = "jobserver" +version = "0.1.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ab46a6e9526ddef3ae7f787c06f0f2600639ba80ea3eade3d8e670a2230f51d6" +dependencies = [ + "libc", +] + +[[package]] +name = "jpeg-decoder" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f5d4a7da358eff58addd2877a45865158f0d78c911d43a5784ceb7bbf52833b0" + [[package]] name = "js-sys" version = "0.3.69" @@ -839,24 +1252,60 @@ version = "1.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" +[[package]] +name = "lebe" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "03087c2bad5e1034e8cace5926dec053fb3790248370865f5117a7d0213354c8" + [[package]] name = "libc" version = "0.2.153" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9c198f91728a82281a64e1f4f9eeb25d82cb32a5de251c6bd1b5154d63a8e7bd" +[[package]] +name = "libfuzzer-sys" +version = "0.4.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a96cfd5557eb82f2b83fed4955246c988d331975a002961b07c81584d107e7f7" +dependencies = [ + "arbitrary", + "cc", + "once_cell", +] + [[package]] name = "libm" version = "0.2.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4ec2a862134d2a7d32d7983ddcdd1c4923530833c9f2ea1a44fc5fa473989058" +[[package]] +name = "lock_api" +version = "0.4.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c168f8615b12bc01f9c17e2eb0cc07dcae1940121185446edc3744920e8ef45" +dependencies = [ + "autocfg", + "scopeguard", +] + [[package]] name = "log" version = "0.4.21" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "90ed8c1e510134f979dbc4f070f87d4313098b704861a105fe34231c70a3901c" +[[package]] +name = "loop9" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fae87c125b03c1d2c0150c90365d7d6bcc53fb73a9acaef207d2d065860f062" +dependencies = [ + "imgref", +] + [[package]] name = "matchers" version = "0.1.0" @@ -872,6 +1321,16 @@ version = "0.7.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0e7465ac9959cc2b1404e8e2367b43684a6d13790fe23056cc8c6c5a6b7bcb94" +[[package]] +name = "maybe-rayon" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ea1f30cedd69f0a2954655f7188c6a834246d2bcf1e315e2ac40c4b24dc9519" +dependencies = [ + "cfg-if", + "rayon", +] + [[package]] name = "memchr" version = "2.7.1" @@ -885,7 +1344,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0f23f71580015254b020e856feac3df5878c2c7a8812297edd6c0a485ac9dada" dependencies = [ "serde", - "toml", + "toml 0.7.8", ] [[package]] @@ -928,6 +1387,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9d811f3e15f28568be3407c8e7fdb6514c1cda3cb30683f15b6a1a1dc4ea14a7" dependencies = [ "adler", + "simd-adler32", ] [[package]] @@ -941,6 +1401,30 @@ dependencies = [ "windows-sys 0.48.0", ] +[[package]] +name = "multer" +version = "3.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a15d522be0a9c3e46fd2632e272d178f56387bdb5c9fbb3a36c649062e9b5219" +dependencies = [ + "bytes", + "encoding_rs", + "futures-util", + "http", + "httparse", + "log", + "memchr", + "mime", + "spin", + "version_check", +] + +[[package]] +name = "new_debug_unreachable" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "650eef8c711430f1a879fdd01d4745a7deea475becfb90269c06775983bbf086" + [[package]] name = "nom" version = "7.1.3" @@ -951,6 +1435,12 @@ dependencies = [ "minimal-lexical", ] +[[package]] +name = "noop_proc_macro" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0676bb32a98c1a483ce53e500a81ad9c3d5b3f7c920c28c24e9cb0980d0b5bc8" + [[package]] name = "npm_rs" version = "1.0.0" @@ -987,6 +1477,17 @@ version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "51d515d32fb182ee37cda2ccdcb92950d6a3c2893aa280e540671c2cd0f3b1d9" +[[package]] +name = "num-derive" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed3955f1a9c7c0c15e092f9c887db08b1fc683305fdf6eb6684f22555355e202" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.52", +] + [[package]] name = "num-integer" version = "0.1.46" @@ -996,6 +1497,18 @@ dependencies = [ "num-traits", ] +[[package]] +name = "num-rational" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0638a1c9d0a3c0914158145bc76cff373a75a627e6ecbfb71cbe6f453a5a19b0" +dependencies = [ + "autocfg", + "num-bigint", + "num-integer", + "num-traits", +] + [[package]] name = "num-traits" version = "0.2.18" @@ -1047,6 +1560,12 @@ dependencies = [ "subtle", ] +[[package]] +name = "paste" +version = "1.0.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "de3145af08024dea9fa9914f381a17b8fc6034dfb00f3a84013f7ff43f29ed4c" + [[package]] name = "pem" version = "3.0.3" @@ -1080,7 +1599,7 @@ checksum = "2f38a4412a78282e09a2cf38d195ea5420d15ba0602cb375210efbc877243965" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.52", ] [[package]] @@ -1095,12 +1614,37 @@ version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" +[[package]] +name = "pkg-config" +version = "0.3.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d231b230927b5e4ad203db57bbcbee2802f6bce620b1e4a9024a07d94e2907ec" + +[[package]] +name = "png" +version = "0.17.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06e4b0d3d1312775e782c86c91a111aa1f910cbb65e1337f9975b5f9a554b5e1" +dependencies = [ + "bitflags 1.3.2", + "crc32fast", + "fdeflate", + "flate2", + "miniz_oxide", +] + [[package]] name = "powerfmt" version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" +[[package]] +name = "ppv-lite86" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b40af805b3121feab8a3c29f04d8ad262fa8e0561883e7653e024ae4479e6de" + [[package]] name = "pq-sys" version = "0.4.8" @@ -1110,6 +1654,30 @@ dependencies = [ "vcpkg", ] +[[package]] +name = "proc-macro-error" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da25490ff9892aab3fcf7c36f08cfb902dd3e71ca0f9f9517bea02a73a5ce38c" +dependencies = [ + "proc-macro-error-attr", + "proc-macro2", + "quote", + "syn 1.0.109", + "version_check", +] + +[[package]] +name = "proc-macro-error-attr" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1be40180e52ecc98ad80b184934baf3d0d29f979574e439af5a55274b35f869" +dependencies = [ + "proc-macro2", + "quote", + "version_check", +] + [[package]] name = "proc-macro2" version = "1.0.79" @@ -1119,6 +1687,40 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "profiling" +version = "1.0.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "43d84d1d7a6ac92673717f9f6d1518374ef257669c24ebc5ac25d5033828be58" +dependencies = [ + "profiling-procmacros", +] + +[[package]] +name = "profiling-procmacros" +version = "1.0.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8021cf59c8ec9c432cfc2526ac6b8aa508ecaf29cd415f271b8406c1b851c3fd" +dependencies = [ + "quote", + "syn 2.0.52", +] + +[[package]] +name = "qoi" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f6d64c71eb498fe9eae14ce4ec935c555749aef511cca85b5568910d6e48001" +dependencies = [ + "bytemuck", +] + +[[package]] +name = "quick-error" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a993555f31e5a609f617c12db6250dedcac1b0a85076912c436e6fc9b2c8e6a3" + [[package]] name = "quote" version = "1.0.35" @@ -1128,6 +1730,27 @@ dependencies = [ "proc-macro2", ] +[[package]] +name = "rand" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" +dependencies = [ + "libc", + "rand_chacha", + "rand_core", +] + +[[package]] +name = "rand_chacha" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" +dependencies = [ + "ppv-lite86", + "rand_core", +] + [[package]] name = "rand_core" version = "0.6.4" @@ -1137,6 +1760,76 @@ dependencies = [ "getrandom", ] +[[package]] +name = "rav1e" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cd87ce80a7665b1cce111f8a16c1f3929f6547ce91ade6addf4ec86a8dda5ce9" +dependencies = [ + "arbitrary", + "arg_enum_proc_macro", + "arrayvec", + "av1-grain", + "bitstream-io", + "built", + "cfg-if", + "interpolate_name", + "itertools", + "libc", + "libfuzzer-sys", + "log", + "maybe-rayon", + "new_debug_unreachable", + "noop_proc_macro", + "num-derive", + "num-traits", + "once_cell", + "paste", + "profiling", + "rand", + "rand_chacha", + "simd_helpers", + "system-deps", + "thiserror", + "v_frame", + "wasm-bindgen", +] + +[[package]] +name = "ravif" +version = "0.11.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bc13288f5ab39e6d7c9d501759712e6969fcc9734220846fc9ed26cae2cc4234" +dependencies = [ + "avif-serialize", + "imgref", + "loop9", + "quick-error", + "rav1e", + "rayon", + "rgb", +] + +[[package]] +name = "rayon" +version = "1.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b418a60154510ca1a002a752ca9714984e21e4241e804d32555251faf8b78ffa" +dependencies = [ + "either", + "rayon-core", +] + +[[package]] +name = "rayon-core" +version = "1.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1465873a3dfdaa8ae7cb14b4383657caab0b3e8a0aa9ae8e04b044854c8dfce2" +dependencies = [ + "crossbeam-deque", + "crossbeam-utils", +] + [[package]] name = "regex" version = "1.10.3" @@ -1181,6 +1874,15 @@ version = "0.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c08c74e62047bb2de4ff487b251e4a92e24f48745648451635cec7d591162d9f" +[[package]] +name = "rgb" +version = "0.8.37" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05aaa8004b64fd573fc9d002f4e632d51ad4f026c2b5ba95fcb6c2f32c2c47d8" +dependencies = [ + "bytemuck", +] + [[package]] name = "ring" version = "0.17.8" @@ -1216,7 +1918,7 @@ dependencies = [ "proc-macro2", "quote", "rust-embed-utils", - "syn", + "syn 2.0.52", "walkdir", ] @@ -1257,6 +1959,12 @@ dependencies = [ "winapi-util", ] +[[package]] +name = "scopeguard" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" + [[package]] name = "serde" version = "1.0.197" @@ -1274,7 +1982,7 @@ checksum = "7eb0b34b42edc17f6b7cac84a52a1c5f0e1bb2227e997ca9011ea3dd34e8610b" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.52", ] [[package]] @@ -1339,6 +2047,21 @@ dependencies = [ "lazy_static", ] +[[package]] +name = "simd-adler32" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d66dc143e6b11c1eddc06d5c423cfc97062865baf299914ab64caa38182078fe" + +[[package]] +name = "simd_helpers" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "95890f873bec569a0362c235787f3aca6e1e887302ba4840839bcc6459c42da6" +dependencies = [ + "quote", +] + [[package]] name = "simple_asn1" version = "0.6.2" @@ -1381,6 +2104,27 @@ name = "spin" version = "0.9.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6980e8d7511241f8acf4aebddbb1ff938df5eebe98691418c4468d0b72a96a67" +dependencies = [ + "lock_api", +] + +[[package]] +name = "sqids" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f328f10ae594f0da04e5b2f82c089232697312661bca22d5d015a680c84639d" +dependencies = [ + "derive_builder", + "serde", + "serde_json", + "thiserror", +] + +[[package]] +name = "strsim" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623" [[package]] name = "subtle" @@ -1388,6 +2132,16 @@ version = "2.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "81cdd64d312baedb58e21336b31bc043b77e01cc99033ce76ef539f78e965ebc" +[[package]] +name = "syn" +version = "1.0.109" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237" +dependencies = [ + "proc-macro2", + "unicode-ident", +] + [[package]] name = "syn" version = "2.0.52" @@ -1405,6 +2159,25 @@ version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2047c6ded9c721764247e62cd3b03c09ffc529b2ba5b10ec482ae507a4a70160" +[[package]] +name = "system-deps" +version = "6.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a3e535eb8dded36d55ec13eddacd30dec501792ff23a0b1682c38601b8cf2349" +dependencies = [ + "cfg-expr", + "heck 0.5.0", + "pkg-config", + "toml 0.8.12", + "version-compare", +] + +[[package]] +name = "target-lexicon" +version = "0.12.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e1fc403891a21bcfb7c37834ba66a547a8f402146eba7265b5a6d88059c9ff2f" + [[package]] name = "thiserror" version = "1.0.58" @@ -1422,7 +2195,7 @@ checksum = "c61f3ba182994efc43764a46c018c347bc492c79f024e705f46567b418f6d4f7" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.52", ] [[package]] @@ -1435,6 +2208,17 @@ dependencies = [ "once_cell", ] +[[package]] +name = "tiff" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba1310fcea54c6a9a4fd1aad794ecc02c31682f6bfbecdf460bf19533eed1e3e" +dependencies = [ + "flate2", + "jpeg-decoder", + "weezl", +] + [[package]] name = "time" version = "0.3.34" @@ -1491,7 +2275,7 @@ checksum = "5b8a1e28f2deaa14e508979454cb3a223b10b938b45af148bc0986de36f1923b" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.52", ] [[package]] @@ -1517,7 +2301,19 @@ dependencies = [ "serde", "serde_spanned", "toml_datetime", - "toml_edit", + "toml_edit 0.19.15", +] + +[[package]] +name = "toml" +version = "0.8.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e9dd1545e8208b4a5af1aa9bbd0b4cf7e9ea08fabc5d0a5c67fcaafa17433aa3" +dependencies = [ + "serde", + "serde_spanned", + "toml_datetime", + "toml_edit 0.22.9", ] [[package]] @@ -1539,7 +2335,20 @@ dependencies = [ "serde", "serde_spanned", "toml_datetime", - "winnow", + "winnow 0.5.40", +] + +[[package]] +name = "toml_edit" +version = "0.22.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e40bb779c5187258fd7aad0eb68cb8706a0a81fa712fbea808ab43c4b8374c4" +dependencies = [ + "indexmap", + "serde", + "serde_spanned", + "toml_datetime", + "winnow 0.6.5", ] [[package]] @@ -1564,12 +2373,16 @@ version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1e9cd434a998747dd2c4276bc96ee2e0c7a2eadf3cae88e52be55a05fa9053f5" dependencies = [ - "bitflags", + "async-compression", + "bitflags 2.4.2", "bytes", + "futures-core", "http", "http-body", "http-body-util", "pin-project-lite", + "tokio", + "tokio-util", "tower-layer", "tower-service", "tracing", @@ -1607,7 +2420,7 @@ checksum = "34704c8d6ebcbc939824180af020566b01a7c01f80641264eba0999f6c2b6be7" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.52", ] [[package]] @@ -1676,6 +2489,43 @@ version = "0.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" +[[package]] +name = "utoipa" +version = "4.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "272ebdfbc99111033031d2f10e018836056e4d2c8e2acda76450ec7974269fa7" +dependencies = [ + "indexmap", + "serde", + "serde_json", + "utoipa-gen", +] + +[[package]] +name = "utoipa-gen" +version = "4.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3c9f4d08338c1bfa70dde39412a040a884c6f318b3d09aaaf3437a1e52027fc" +dependencies = [ + "proc-macro-error", + "proc-macro2", + "quote", + "regex", + "syn 2.0.52", +] + +[[package]] +name = "utoipa-rapidoc" +version = "3.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4e79a75396496e4fe41359375a67e8fcb9c28444cd1cb5e8ac54f47684e64290" +dependencies = [ + "axum", + "serde", + "serde_json", + "utoipa", +] + [[package]] name = "uuid" version = "1.7.0" @@ -1686,6 +2536,17 @@ dependencies = [ "serde", ] +[[package]] +name = "v_frame" +version = "0.3.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d6f32aaa24bacd11e488aa9ba66369c7cd514885742c9fe08cfe85884db3e92b" +dependencies = [ + "aligned-vec", + "num-traits", + "wasm-bindgen", +] + [[package]] name = "valuable" version = "0.1.0" @@ -1698,6 +2559,12 @@ version = "0.2.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" +[[package]] +name = "version-compare" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "852e951cb7832cb45cb1169900d19760cfa39b82bc0ea9c0e5a14ae88411c98b" + [[package]] name = "version_check" version = "0.9.4" @@ -1741,7 +2608,7 @@ dependencies = [ "once_cell", "proc-macro2", "quote", - "syn", + "syn 2.0.52", "wasm-bindgen-shared", ] @@ -1763,7 +2630,7 @@ checksum = "e94f17b526d0a461a191c78ea52bbce64071ed5c04c9ffe424dcb38f74171bb7" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.52", "wasm-bindgen-backend", "wasm-bindgen-shared", ] @@ -1774,6 +2641,12 @@ version = "0.2.92" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "af190c94f2773fdb3729c55b007a722abb5384da03bc0986df4c289bf5567e96" +[[package]] +name = "weezl" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53a85b86a771b1c87058196170769dd264f66c0782acf1ae6cc51bfd64b39082" + [[package]] name = "winapi" version = "0.3.9" @@ -1954,3 +2827,36 @@ checksum = "f593a95398737aeed53e489c785df13f3618e41dbcd6718c6addbf1395aa6876" dependencies = [ "memchr", ] + +[[package]] +name = "winnow" +version = "0.6.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dffa400e67ed5a4dd237983829e66475f0a4a26938c4b04c21baede6262215b8" +dependencies = [ + "memchr", +] + +[[package]] +name = "zune-core" +version = "0.4.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f423a2c17029964870cfaabb1f13dfab7d092a62a29a89264f4d36990ca414a" + +[[package]] +name = "zune-inflate" +version = "0.2.54" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "73ab332fe2f6680068f3582b16a24f90ad7096d5d39b974d1c0aff0125116f02" +dependencies = [ + "simd-adler32", +] + +[[package]] +name = "zune-jpeg" +version = "0.4.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec866b44a2a1fd6133d363f073ca1b179f438f99e7e5bfb1e33f7181facfe448" +dependencies = [ + "zune-core", +] diff --git a/Cargo.toml b/Cargo.toml index 7770e05..f1063eb 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,11 +1,11 @@ [package] -name = "elnafo" +name = "elnafo-backend" version = "0.1.0" edition = "2021" -authors = ["L-Nafaryus "] [dependencies] -axum = { version = "0.7.4", features = [] } +axum = { version = "0.7.4", features = ["http2", "macros", "multipart"] } tokio = { version = "1.36.0", default-features = false, features = [ "macros", "fs", @@ -31,10 +31,23 @@ rand_core = { version = "0.6.4", features = ["std"] } chrono = { version = "0.4.35", features = ["serde"] } jsonwebtoken = "9.2.0" axum-extra = { version = "0.9.2", features = ["cookie"] } -tower-http = { version = "0.5.2", features = ["trace", "cors"] } -frontend = { version = "0.1.0", path = "crates/frontend" } +tower-http = { version = "0.5.2", features = [ + "trace", + "cors", + "compression-gzip", + "decompression-gzip", +] } +elnafo-frontend = { version = "0.1.0", path = "crates/elnafo-frontend" } mime_guess = "2.0.4" +sqids = "0.4.1" +image = "0.25.1" +toml = "0.8.12" +glob = "0.3.1" +deadpool-sync = "0.1.2" +utoipa = { version = "4.2.0", features = ["axum_extras"] } +utoipa-rapidoc = { version = "3.0.0", features = ["axum"] } +deadpool = "0.11.1" [workspace] -members = ["crates/frontend"] +members = ["crates/elnafo-frontend"] resolver = "2" diff --git a/src/api/doc.rs b/src/api/doc.rs new file mode 100644 index 0000000..3490631 --- /dev/null +++ b/src/api/doc.rs @@ -0,0 +1,58 @@ +use utoipa::{ + openapi::{ + security::{ApiKey, ApiKeyValue, HttpAuthScheme, HttpBuilder, SecurityScheme}, + Components, + }, + Modify, OpenApi, +}; + +use super::errors; +use super::user; + +#[derive(OpenApi)] +#[openapi( + paths( + super::healthcheck, + user::all, + user::register, + user::remove, + user::login, + user::logout, + user::profile, + user::current, + user::avatar + ), + components(schemas( + crate::db::errors::DatabaseError, + user::UserError, + user::schema::NewUser, + user::schema::User, + user::schema::RemoveUser, + user::schema::LoginUser, + user::schema::Avatar, + user::schema::Image, + errors::ApiError + )), + modifiers(&SecurityAddon) +)] +pub struct ApiDoc; + +pub struct SecurityAddon; + +impl Modify for SecurityAddon { + fn modify(&self, openapi: &mut utoipa::openapi::OpenApi) { + if openapi.components.is_none() { + openapi.components = Some(Components::new()); + } + + openapi.components.as_mut().unwrap().add_security_scheme( + "token", + SecurityScheme::Http( + HttpBuilder::new() + .scheme(HttpAuthScheme::Bearer) + .bearer_format("JWT") + .build(), + ), + ); + } +} diff --git a/src/api/errors.rs b/src/api/errors.rs new file mode 100644 index 0000000..6e848fe --- /dev/null +++ b/src/api/errors.rs @@ -0,0 +1,97 @@ +use std::any::Any; + +use axum::{http::StatusCode, response::IntoResponse, Json}; +use serde_json::json; + +use crate::db::errors::DatabaseError; + +use super::user::UserError; + +#[derive(Debug, utoipa::ToSchema)] +pub enum ApiError { + Database(DatabaseError), + AuthError(AuthError), + ReadContent, + Query(UserError), +} + +impl std::error::Error for ApiError {} + +impl std::fmt::Display for ApiError { + fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { + match self { + Self::Database(ref e) => e.fmt(f), + Self::AuthError(e) => write!(f, "Authentication error occured: {}", e), + Self::ReadContent => write!(f, "Failed to read body content"), + Self::Query(ref e) => e.fmt(f), + } + } +} + +impl From for ApiError { + fn from(e: DatabaseError) -> Self { + Self::Database(e) + } +} + +impl IntoResponse for ApiError { + fn into_response(self) -> axum::response::Response { + let status = match self { + Self::Database(_) => StatusCode::INTERNAL_SERVER_ERROR, + Self::AuthError(_) => StatusCode::UNAUTHORIZED, + Self::ReadContent => StatusCode::UNPROCESSABLE_ENTITY, + Self::Query(ref e) => match e { + UserError::Exists => StatusCode::CONFLICT, + UserError::HashPassword | UserError::ParseUuid => StatusCode::INTERNAL_SERVER_ERROR, + UserError::MissedCredentials + | UserError::InvalidCredentials + | UserError::Unauthorized => StatusCode::UNAUTHORIZED, + UserError::NotFound => StatusCode::NOT_FOUND, + }, + }; + + (status, format!("{}", self)).into_response() + } +} + +#[derive(Debug)] +pub enum AuthError { + MissingCredentials, + InvalidCredentials, + MissingToken, + InvalidToken, + MissingUser, +} + +impl std::error::Error for AuthError {} + +impl std::fmt::Display for AuthError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Self::MissingCredentials => write!(f, "Missing credentials"), + Self::InvalidCredentials => write!(f, "Invalid credentials"), + Self::MissingToken => write!(f, "Missing token"), + Self::InvalidToken => write!(f, "Invalid token"), + Self::MissingUser => write!(f, "Missing user"), + } + } +} + +impl From for ApiError { + fn from(e: AuthError) -> Self { + Self::AuthError(e) + } +} + +impl IntoResponse for AuthError { + fn into_response(self) -> axum::response::Response { + let status = match self { + Self::MissingCredentials | Self::MissingToken => StatusCode::BAD_REQUEST, + Self::InvalidCredentials | Self::InvalidToken | Self::MissingUser => { + StatusCode::UNAUTHORIZED + } + }; + + (status, format!("{}", self)).into_response() + } +} diff --git a/src/api/v1/middleware.rs b/src/api/middleware.rs similarity index 78% rename from src/api/v1/middleware.rs rename to src/api/middleware.rs index 5ca650a..f9c1bf2 100644 --- a/src/api/v1/middleware.rs +++ b/src/api/middleware.rs @@ -10,17 +10,22 @@ use axum::{ }; use axum_extra::extract::CookieJar; -use crate::{db::user::User, state::AppState}; +use crate::{ + db::{self, schema::users, user::User}, + state::AppState, +}; use super::errors::AuthError; -use super::token::TokenClaims; +use super::{errors::ApiError, token::TokenClaims}; pub async fn jwt( cookie_jar: CookieJar, State(state): State>, mut req: Request, next: Next, -) -> Result> { +) -> Result { + use diesel::prelude::*; + let token = cookie_jar .get("token") .map(|cookie| cookie.value().to_string()) @@ -32,17 +37,22 @@ pub async fn jwt( .map(|auth_token| auth_token.to_owned()) }); - let token = token.ok_or_else(|| AuthError::MissingToken)?; + let token = token.ok_or(AuthError::MissingToken)?; let claims = TokenClaims::validate(token, state.config.jwt.secret.to_owned()) .map_err(|_| AuthError::InvalidToken)?; let user_id = uuid::Uuid::parse_str(&claims.sub).map_err(|_| AuthError::InvalidToken)?; - let user = User::find(&state.database, User::by_id(user_id)) - .await - .map_err(AuthError::InternalError)?; + let user = db::execute(&state.database, move |conn| { + users::table + .into_boxed() + .filter(users::id.eq(user_id)) + .first::(conn) + .optional() + }) + .await?; - let user = user.ok_or_else(|| AuthError::MissingUser)?; + let user = user.ok_or(AuthError::MissingUser)?; req.extensions_mut().insert(user); Ok(next.run(req).await) diff --git a/src/api/mod.rs b/src/api/mod.rs index a3a6d96..27b1f03 100644 --- a/src/api/mod.rs +++ b/src/api/mod.rs @@ -1 +1,76 @@ -pub mod v1; +pub mod doc; +pub mod errors; +pub mod middleware; +pub mod token; +pub mod user; + +use std::sync::Arc; + +use axum::{ + extract::DefaultBodyLimit, + http::{header::*, Method, StatusCode}, + response::IntoResponse, + routing::{get, post}, + Json, Router, +}; +use serde_json::json; +use tower_http::cors::CorsLayer; + +use crate::state::AppState; + +pub fn routes(state: Arc) -> Router { + let cors = CorsLayer::new() + .allow_methods([Method::GET, Method::POST, Method::OPTIONS]) + .allow_headers(vec![ORIGIN, AUTHORIZATION, ACCEPT, CONTENT_TYPE, COOKIE]) + .allow_origin([ + "http://localhost:54600".parse().unwrap(), + "http://localhost:5173".parse().unwrap(), + ]) + .allow_credentials(true); + + let jwt = axum::middleware::from_fn_with_state(state.to_owned(), middleware::jwt_auth); + + Router::new() + .route("/healthcheck", get(healthcheck)) + .route("/user/all", get(user::all)) + .route("/user/register", post(user::register)) + .route("/user/remove", post(user::remove)) + .route("/user/login", post(user::login)) + .route("/user/logout", get(user::logout)) + .route( + "/user/current", + get(user::current).route_layer(jwt.to_owned()), + ) + .route( + "/user/:login", + get(user::profile).route_layer(jwt.to_owned()), + ) + .route( + "/user/avatar", + post(user::avatar) + .route_layer(jwt) + .layer(DefaultBodyLimit::max(10 * 10000)), + ) + .layer(cors) + .fallback(fallback) + .with_state(state) +} + +#[utoipa::path(get, path = "/api/healthcheck", responses((status = 200, body = String)))] +pub async fn healthcheck() -> impl IntoResponse { + ( + StatusCode::OK, + Json(json!({ + "status": StatusCode::OK.to_string(), + })), + ) +} + +pub async fn fallback() -> impl IntoResponse { + ( + StatusCode::NOT_FOUND, + Json(json!({ + "status": StatusCode::NOT_FOUND.to_string(), + })), + ) +} diff --git a/src/api/v1/token.rs b/src/api/token.rs similarity index 100% rename from src/api/v1/token.rs rename to src/api/token.rs diff --git a/src/api/user.rs b/src/api/user.rs new file mode 100644 index 0000000..4ff3882 --- /dev/null +++ b/src/api/user.rs @@ -0,0 +1,442 @@ +use argon2::{password_hash::SaltString, Argon2, PasswordHasher}; +use argon2::{PasswordHash, PasswordVerifier}; +use axum::body::Bytes; +use axum::extract::{Multipart, Path}; +use axum::http::HeaderValue; +use axum::response::Response; +use axum::Extension; +use axum::{ + extract::State, + http::{header, StatusCode}, + response::IntoResponse, + Json, +}; +use axum_extra::extract::cookie::{Cookie, SameSite}; +use rand_core::OsRng; +use std::collections::HashSet; +use std::sync::Arc; + +use crate::config::Config; +use crate::state::AppState; +use crate::{ + db, + db::schema::users, + db::user::{NewUser, User}, +}; + +use super::errors::ApiError; +use super::token::TokenClaims; + +#[derive(Debug, utoipa::ToSchema)] +pub enum UserError { + Exists, + HashPassword, + ParseUuid, + MissedCredentials, + InvalidCredentials, + NotFound, + Unauthorized, +} + +impl std::error::Error for UserError {} + +impl std::fmt::Display for UserError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Self::Exists => write!(f, "User already exists"), + Self::HashPassword => write!(f, "Failed to create a password hash"), + Self::ParseUuid => write!(f, "Failed to parse user UUID"), + Self::MissedCredentials => write!(f, "Missed user credentials"), + Self::InvalidCredentials => write!(f, "Invalid user credentials"), + Self::NotFound => write!(f, "User not found"), + Self::Unauthorized => write!(f, "User is not authorized"), + } + } +} + +pub mod schema { + use crate::db::user; + + #[derive(serde::Serialize, serde::Deserialize, utoipa::ToSchema)] + pub struct NewUser { + pub login: String, + pub password: String, + pub email: String, + } + + #[derive(Debug, serde::Serialize, utoipa::ToSchema)] + pub struct User { + pub id: String, + pub login: String, + pub name: String, + pub email: String, + pub is_admin: bool, + pub avatar: String, + } + + #[derive(serde::Deserialize, utoipa::ToSchema)] + pub struct RemoveUser { + pub id: String, + } + + #[derive(serde::Deserialize, utoipa::ToSchema)] + pub struct LoginUser { + pub email: Option, + pub login: Option, + pub password: String, + } + + #[derive(serde::Deserialize, utoipa::ToSchema)] + pub struct Avatar { + pub content: String, + pub mime: String, + } + + impl User { + pub fn from(user: &user::User) -> Self { + User { + id: user.id.to_string(), + login: user.login.to_string(), + name: user.name.to_owned(), + email: user.email.to_owned(), + is_admin: user.is_admin, + avatar: user.avatar.to_owned(), + } + } + } + + #[derive(utoipa::ToSchema)] + pub struct Image { + #[schema(value_type = String, format = Binary)] + pub file_content: Vec, + } +} + +#[utoipa::path(get, path = "/api/user/all", + responses((status = 200, body = [User])) +)] +pub async fn all(State(state): State>) -> Result>, ApiError> { + use diesel::prelude::*; + + let users = db::execute(&state.database, move |conn| { + users::table.select(User::as_select()).get_results(conn) + }) + .await? + .into_iter() + .map(|ref user| schema::User::from(user)) + .collect::>(); + + Ok(Json(users)) +} + +#[utoipa::path(post, path = "/api/user/register", + request_body = NewUser, + responses((status = 200, body = User), (status = 500, body = ApiError)) +)] +pub async fn register( + State(state): State>, + Json(body): Json, +) -> Result, ApiError> { + use diesel::prelude::*; + + let count = db::execute(&state.database, move |conn| { + users::table.into_boxed().count().get_result::(conn) + }) + .await?; + + let (login, email) = (body.login.clone(), body.email.clone()); + let user = db::execute(&state.database, move |conn| { + users::table + .into_boxed() + .filter(users::login.eq(login).or(users::email.eq(email))) + .first::(conn) + .optional() + }) + .await?; + + if user.is_some() { + return Err(ApiError::Query(UserError::Exists)); + } + + let hashed_password = Argon2::default() + .hash_password(body.password.as_bytes(), &SaltString::generate(&mut OsRng)) + .map_err(|_| ApiError::Query(UserError::HashPassword)) + .map(|hash| hash.to_string())?; + + let new_user = NewUser { + login: body.login.clone(), + hashed_password, + name: body.login, + email: body.email, + is_admin: count == 0, + avatar: String::default(), + }; + + let user = db::execute(&state.database, move |conn| { + diesel::insert_into(users::table) + .values(new_user) + .returning(User::as_returning()) + .get_result(conn) + }) + .await?; + + Ok(Json(schema::User::from(&user))) +} + +#[utoipa::path(post, path = "/api/user/remove", + request_body = RemoveUser, + responses((status = 200)) +)] +pub async fn remove( + State(state): State>, + Json(body): Json, +) -> Result<(), ApiError> { + use diesel::prelude::*; + + let uuid = + uuid::Uuid::parse_str(&body.id).map_err(|_| ApiError::Query(UserError::ParseUuid))?; + + db::execute(&state.database, move |conn| { + diesel::delete(users::table.filter(users::id.eq(uuid))).execute(conn) + }) + .await?; + + Ok(()) +} + +#[utoipa::path(post, path = "/api/user/login", + request_body = LoginUser, + responses((status = 200, body = User), (status = "4XX", body = UserError), (status = 500, body = ApiError)) +)] +pub async fn login( + State(state): State>, + Json(body): Json, +) -> Result { + use diesel::prelude::*; + + let query = users::table.into_boxed().select(User::as_select()); + let query = if let Some(login) = body.login { + query.filter(users::login.eq(login)) + } else if let Some(email) = body.email { + query.filter(users::email.eq(email)) + } else { + return Err(ApiError::Query(UserError::MissedCredentials)); + }; + + let user = db::execute(&state.database, move |conn| { + query.first::(conn).optional() + }) + .await?; + + let user = match user { + Some(user) => user, + None => return Err(ApiError::Query(UserError::InvalidCredentials)), + }; + + if !match PasswordHash::new(&user.hashed_password) { + Ok(parsed_hash) => Argon2::default() + .verify_password(body.password.as_bytes(), &parsed_hash) + .map_or(false, |_| true), + Err(_) => false, + } { + return Err(ApiError::Query(UserError::InvalidCredentials)); + } + + let token = TokenClaims::create( + user.id.to_string(), + state.config.jwt.secret.to_owned(), + state.config.jwt.maxage, + ) + .unwrap(); + + let cookie = Cookie::build(("token", token.to_owned())) + .path("/") + .max_age(time::Duration::hours(1)) + .same_site(SameSite::None) + .secure(true) + .http_only(true); + + let mut response = Json(schema::User::from(&user)).into_response(); + response + .headers_mut() + .insert(header::SET_COOKIE, cookie.to_string().parse().unwrap()); + + Ok(response) +} + +#[utoipa::path(get, path = "/api/user/logout", responses((status = 200)))] +pub async fn logout() -> Result { + let cookie = Cookie::build(("token", "")) + .path("/") + .max_age(time::Duration::hours(-1)) + .same_site(SameSite::None) + .secure(true) + .http_only(true); + + let response = Response::builder() + .status(StatusCode::OK) + .header( + header::SET_COOKIE, + cookie.to_string().parse::().unwrap(), + ) + .body(axum::body::Body::empty()) + .unwrap(); + + Ok(response) +} + +#[utoipa::path(get, path = "/api/user/{login}", + params(("login", Path,)), + responses((status = 404, body = UserError), (status = 500, body = ApiError)) +)] +pub async fn profile( + State(state): State>, + Extension(user_id): Extension>, + Path(login): Path, +) -> Result { + use diesel::prelude::*; + + // TODO: Current user priveleges + let user = db::execute(&state.database, move |conn| { + users::table + .into_boxed() + .filter(users::login.eq(login)) + .first::(conn) + .optional() + }) + .await?; + + match user { + Some(user) => Ok(Json(schema::User::from(&user))), + None => Err(ApiError::Query(UserError::NotFound)), + } +} + +#[utoipa::path(get, path = "/api/user/current", + security(("token" = [])) +)] +pub async fn current( + State(state): State>, + Extension(user_id): Extension>, +) -> Result { + use diesel::prelude::*; + + let uuid = match user_id { + Some(user_id) => user_id, + None => return Err(ApiError::Query(UserError::Unauthorized)), + }; + + let user = db::execute(&state.database, move |conn| { + users::table + .into_boxed() + .filter(users::id.eq(uuid)) + .first::(conn) + .optional() + }) + .await?; + + match user { + Some(user) => Ok(Json(schema::User::from(&user))), + None => Err(ApiError::Query(UserError::NotFound)), + } +} + +#[axum::debug_handler] +#[utoipa::path(post, path = "/api/user/avatar", + security(("token" = [])), + request_body(content = Image, content_type = "multipart/form-data"), + responses((status = 200), (status = "4XX", body = UserError), (status = 500, body = ApiError)) +)] +pub async fn avatar( + State(state): State>, + Extension(user_id): Extension>, + //Json(body): Json, + mut multipart: Multipart, +) -> Result { + use diesel::prelude::*; + + let uuid = match user_id { + Some(user_id) => user_id, + None => return Err(ApiError::Query(UserError::Unauthorized)), + }; + + let user = db::execute(&state.database, move |conn| { + users::table + .into_boxed() + .filter(users::id.eq(uuid)) + .first::(conn) + .optional() + }) + .await?; + + let user = match user { + Some(user) => user, + None => return Err(ApiError::Query(UserError::NotFound)), + }; + + let data: Bytes = if let Some(field) = multipart + .next_field() + .await + .map_err(|_| ApiError::ReadContent)? + { + /*if field.name().unwrap() != "file" { + continue; + }*/ + field.bytes().await.map_err(|_| ApiError::ReadContent)? + } else { + return Err(ApiError::ReadContent); + }; + + let avatars = db::execute(&state.database, move |conn| { + users::table + .into_boxed() + .select(users::avatar) + .get_results::(conn) + }) + .await? + .into_iter() + .filter(|avatar_hash| !avatar_hash.is_empty()) + .collect::>(); + + let avatar_id = sqids::Sqids::builder() + .min_length(10) + .blocklist(HashSet::from_iter(avatars.clone().into_iter())) + .build() + .unwrap() + .encode(&[avatars.len() as u64]) + .unwrap(); + + let reader = image::io::Reader::new(std::io::Cursor::new(data)) + .with_guessed_format() + .unwrap(); + let format = reader.format().unwrap(); + let img = reader.decode().unwrap(); + + img.save_with_format( + Config::data_dir() + .unwrap() + .join("avatars") + .join(avatar_id.clone()), + format, + ) + .unwrap(); + + if !user.avatar.is_empty() { + std::fs::remove_file( + Config::data_dir() + .unwrap() + .join("avatars") + .join(user.avatar.clone()), + ) + .unwrap(); + } + + db::execute(&state.database, move |conn| { + diesel::update(&user) + .set(users::avatar.eq(avatar_id)) + .execute(conn) + }) + .await?; + + Ok(()) +} diff --git a/src/api/v1/errors.rs b/src/api/v1/errors.rs deleted file mode 100644 index dcc1dff..0000000 --- a/src/api/v1/errors.rs +++ /dev/null @@ -1,53 +0,0 @@ -use axum::{http::StatusCode, response::IntoResponse, Json}; -use serde_json::json; - -#[derive(Debug)] -pub enum AuthError { - InternalError(E), - InternalE, - MissingCredentials, - InvalidCredentials, - MissingToken, - InvalidToken, - MissingUser, -} - -impl IntoResponse for AuthError { - fn into_response(self) -> axum::response::Response { - let (status, message) = match self { - Self::InternalError(e) => (StatusCode::INTERNAL_SERVER_ERROR, format!("Internal Error: {}", e.to_string())), - Self::InternalE => (StatusCode::INTERNAL_SERVER_ERROR, "Internal E".to_string()), - Self::MissingCredentials => { - (StatusCode::BAD_REQUEST, "Missing credentials".to_string()) - } - Self::InvalidCredentials => { - (StatusCode::UNAUTHORIZED, "Invalid credentials".to_string()) - } - Self::MissingToken => (StatusCode::BAD_REQUEST, "Missing token".to_string()), - Self::InvalidToken => (StatusCode::UNAUTHORIZED, "Invalid token".to_string()), - Self::MissingUser => (StatusCode::UNAUTHORIZED, "User not exists".to_string()), - }; - - ( - status, - Json(json!({ - "status": status.to_string(), - "message": message - })), - ) - .into_response() - } -} - -pub fn internal_error(err: E) -> (StatusCode, Json) -where - E: std::error::Error, -{ - ( - StatusCode::INTERNAL_SERVER_ERROR, - Json(json!({ - "status": StatusCode::INTERNAL_SERVER_ERROR.to_string(), - "message": err.to_string() - })), - ) -} diff --git a/src/api/v1/mod.rs b/src/api/v1/mod.rs deleted file mode 100644 index 3a7fef4..0000000 --- a/src/api/v1/mod.rs +++ /dev/null @@ -1,63 +0,0 @@ -pub mod errors; -pub mod middleware; -pub mod token; -pub mod user; - -use std::sync::Arc; - -use axum::{ - http::{header::*, Method, StatusCode}, - response::IntoResponse, - routing::{get, post}, - Json, Router, -}; -use serde_json::json; -use tower_http::cors::CorsLayer; - -use crate::state::AppState; - -pub fn routes(state: Arc) -> Router { - let cors = CorsLayer::new() - .allow_methods([Method::GET, Method::POST, Method::OPTIONS]) - .allow_headers(vec![ORIGIN, AUTHORIZATION, ACCEPT, CONTENT_TYPE, COOKIE]) - .allow_origin([ - "http://localhost:54600".parse().unwrap(), - "http://localhost:5173".parse().unwrap(), - ]) - .allow_credentials(true); - - let jwt = axum::middleware::from_fn_with_state(state.to_owned(), middleware::jwt_auth); - - Router::new() - .route("/v1/healthcheck", get(healthcheck)) - .route("/v1/user/register", post(user::register)) - .route("/v1/user/remove", post(user::remove)) - .route("/v1/user/login", post(user::login)) - .route("/v1/user/logout", get(user::logout)) - .route( - "/v1/user/current", - get(user::current).route_layer(jwt.to_owned()), - ) - .route("/v1/user/:login", get(user::profile).route_layer(jwt)) - .layer(cors) - .fallback(fallback) - .with_state(state) -} - -pub async fn healthcheck() -> impl IntoResponse { - ( - StatusCode::OK, - Json(json!({ - "status": StatusCode::OK.to_string(), - })), - ) -} - -pub async fn fallback() -> impl IntoResponse { - ( - StatusCode::NOT_FOUND, - Json(json!({ - "status": StatusCode::NOT_FOUND.to_string(), - })), - ) -} diff --git a/src/api/v1/user.rs b/src/api/v1/user.rs deleted file mode 100644 index 1a1c5f9..0000000 --- a/src/api/v1/user.rs +++ /dev/null @@ -1,210 +0,0 @@ -use argon2::Argon2; -use argon2::{PasswordHash, PasswordVerifier}; -use axum::extract::Path; -use axum::Extension; -use axum::{ - extract::State, - http::{header, StatusCode}, - response::IntoResponse, - Json, -}; -use axum_extra::extract::cookie::{Cookie, SameSite}; -use serde_json::json; -use std::sync::Arc; - -use crate::db::user::User; -use crate::state::AppState; - -use super::errors::AuthError; -use super::token::TokenClaims; - -#[derive(serde::Deserialize)] -pub struct RegisterUser { - pub login: String, - pub password: String, - pub email: String, -} - -#[derive(serde::Serialize)] -pub struct FilteredUser { - pub id: String, - pub login: String, - pub name: String, - pub email: String, - pub is_admin: bool, -} - -#[derive(serde::Deserialize)] -pub struct RemoveUser { - pub id: String, -} - -impl FilteredUser { - pub fn from(user: &User) -> Self { - FilteredUser { - id: user.id.to_string(), - login: user.login.to_string(), - name: user.name.to_owned(), - email: user.email.to_owned(), - is_admin: user.is_admin, - } - } -} - -#[derive(serde::Deserialize)] -pub struct LoginUser { - pub email: String, - pub password: String, -} - -pub async fn register( - State(state): State>, - Json(body): Json, -) -> Result> { - let user = User::register( - &state.database, - body.login.to_owned(), - body.password, - body.login, //body.name, - body.email, - false, //body.is_admin, - ) - .await - .map_err(AuthError::InternalError)?; - - Ok(Json(json!({ - "status": StatusCode::OK.to_string(), - "user": FilteredUser::from(&user) - }))) -} - -pub async fn remove( - State(state): State>, - Json(body): Json, -) -> Result> { - let user = User::find( - &state.database, - User::by_id(uuid::Uuid::parse_str(&body.id).map_err(|_| AuthError::InvalidCredentials)?), - ) - .await - .map_err(AuthError::InternalError)?; - - let user = match user { - Some(user) => user, - None => return Err(AuthError::MissingUser), - }; - - User::remove(&state.database, user) - .await - .map_err(|_| AuthError::InternalE)?; - - Ok(Json(json!({"status": StatusCode::OK.to_string()}))) -} - -pub async fn login( - State(state): State>, - Json(body): Json, -) -> Result> { - let user = User::find(&state.database, User::by_email(body.email)) - .await - .map_err(AuthError::InternalError)?; - - let user = match user { - Some(user) => user, - None => return Err(AuthError::InvalidCredentials), - }; - - if !match PasswordHash::new(&user.hashed_password) { - Ok(parsed_hash) => Argon2::default() - .verify_password(body.password.as_bytes(), &parsed_hash) - .map_or(false, |_| true), - Err(_) => false, - } { - return Err(AuthError::InvalidCredentials); - } - - let token = TokenClaims::create( - user.id.to_string(), - state.config.jwt.secret.to_owned(), - state.config.jwt.maxage, - ) - .unwrap(); - - let cookie = Cookie::build(("token", token.to_owned())) - .path("/") - .max_age(time::Duration::hours(1)) - .same_site(SameSite::None) - .secure(true) - .http_only(true); - - let mut response = - Json(json!({"status": StatusCode::OK.to_string(), "token": token, "user": json!(FilteredUser::from(&user))})).into_response(); - response - .headers_mut() - .insert(header::SET_COOKIE, cookie.to_string().parse().unwrap()); - - Ok(response) -} - -pub async fn logout() -> Result)> { - let cookie = Cookie::build(("token", "")) - .path("/") - .max_age(time::Duration::hours(-1)) - .same_site(SameSite::None) - .secure(true) - .http_only(true); - - let mut response = Json(json!({"status": StatusCode::OK.to_string()})).into_response(); - response - .headers_mut() - .insert(header::SET_COOKIE, cookie.to_string().parse().unwrap()); - - Ok(response) -} - -pub async fn profile( - State(state): State>, - Extension(user_id): Extension>, - Path(login): Path, -) -> Result)> { - let user = User::find(&state.database, User::by_login(login)) - .await - .map_err(|_| ()) - .unwrap(); - - let response = if let Some(user) = user { - json!({"status": StatusCode::OK.to_string(), "user": json!(FilteredUser::from(&user))}) - } else { - json!({"status": StatusCode::NOT_FOUND.to_string()}) - }; - - Ok(Json(response)) -} - -pub async fn current( - State(state): State>, - Extension(user_id): Extension>, -) -> Result> { - let user = get_user(state, user_id).await?; - - Ok(Json( - json!({"status": StatusCode::OK.to_string(), "user": json!(FilteredUser::from(&user))}), - )) -} - -async fn get_user( - state: Arc, - user_id: Option, -) -> Result> { - let user = if let Some(user_id) = user_id { - User::find(&state.database, User::by_id(user_id)) - .await - .map_err(AuthError::InternalError) - } else { - Err(AuthError::InvalidCredentials) - }; - - let user = user?.ok_or_else(|| AuthError::MissingUser)?; - - Ok(user) -} diff --git a/src/config.rs b/src/config.rs index 0072100..ff2b417 100644 --- a/src/config.rs +++ b/src/config.rs @@ -1,14 +1,15 @@ use dotenvy::dotenv; -use std::env; +use serde::{Deserialize, Serialize}; +use std::{env, fs}; -#[derive(Debug, Clone)] +#[derive(Debug, Clone, Serialize, Deserialize)] pub struct Config { pub database: Database, pub server: Server, pub jwt: Jwt, } -#[derive(Debug, Clone)] +#[derive(Debug, Clone, Serialize, Deserialize)] pub struct Database { pub host: String, pub port: i32, @@ -17,55 +18,141 @@ pub struct Database { pub name: String, } -#[derive(Debug, Clone)] +#[derive(Debug, Clone, Serialize, Deserialize)] pub struct Server { pub address: String, pub port: i32, } -#[derive(Debug, Clone)] +#[derive(Debug, Clone, Serialize, Deserialize)] pub struct Jwt { pub secret: String, pub expires_in: String, pub maxage: i64, } +fn evar(key: &str) -> Result { + env::var(format!("ELNAFO_{}", key)) +} + impl Config { pub fn new() -> Self { + Config::default() + } + + pub fn with_env(&mut self) -> Result<&Self, ConfigError> { dotenv().ok(); - Config { - database: Database { - host: env::var("DATABASE_HOST").unwrap_or("localhost".to_string()), - port: env::var("DATABASE_PORT") - .unwrap_or("5432".to_string()) - .parse() - .unwrap(), - user: env::var("DATABASE_USER").unwrap_or("elnafo".to_string()), - password: env::var("DATABASE_PASSWORD").unwrap_or("test".to_string()), - name: env::var("DATABASE_NAME").unwrap_or("elnafo".to_string()), - }, - server: Server { - address: env::var("SERVER_ADDRESS").unwrap_or("127.0.0.1".to_string()), - port: env::var("SERVER_PORT") - .unwrap_or("54600".to_string()) - .parse() - .unwrap(), - }, - jwt: Jwt { - secret: env::var("JWT_SECRET").unwrap_or("change_this_secret".to_string()), - expires_in: env::var("JWT_EXPIRES_IN").unwrap_or("60m".to_string()), - maxage: env::var("JWT_MAXAGE") - .unwrap_or("3600".to_string()) - .parse() - .unwrap(), - }, + self.database.host = evar("DATABASE_HOST").unwrap_or(self.database.host.to_owned()); + self.database.port = evar("DATABASE_PORT") + .unwrap_or(self.database.port.to_string()) + .parse()?; + self.database.user = evar("DATABASE_USER").unwrap_or(self.database.user.to_owned()); + self.database.password = + evar("DATABASE_PASSWORD").unwrap_or(self.database.password.to_owned()); + self.database.name = evar("DATABASE_NAME").unwrap_or(self.database.name.to_owned()); + + Ok(self) + } + + pub fn open(path: &std::path::Path) -> Result { + fs::read_to_string(path)?.parse() + } + + pub fn data_dir() -> Result { + let cwd = std::env::current_dir()?; + if cfg!(debug_assertions) { + Ok(cwd.join("temp")) + } else { + Ok(cwd) } } + + pub fn to_string(&self) -> Result { + Ok(toml::to_string(self)?) + } + + pub fn write(&self, path: &std::path::Path) -> Result<(), ConfigError> { + Ok(fs::write(path, self.to_string()?)?) + } + + pub fn database_url(&self) -> String { + format!( + "postgres://{}:{}@{}:{}/{}", + self.database.user, + self.database.password, + self.database.host, + self.database.port, + self.database.name + ) + } } impl Default for Config { fn default() -> Self { - Config::new() + Config { + database: Database { + host: String::from("localhost"), + port: 5432, + user: String::from("elnafo"), + password: String::from("test"), + name: String::from("elnafo"), + }, + server: Server { + address: String::from("127.0.0.1"), + port: 54600, + }, + jwt: Jwt { + secret: String::from("change_this_secret"), + expires_in: String::from("60m"), + maxage: 3600, + }, + } + } +} + +impl std::str::FromStr for Config { + type Err = ConfigError; + fn from_str(s: &str) -> Result { + toml::from_str(s).map_err(|_| ConfigError::Parse) + } +} + +#[derive(Debug)] +pub enum ConfigError { + Parse, + StringParse, + Serialize, + IO, +} + +impl std::error::Error for ConfigError {} + +impl std::fmt::Display for ConfigError { + fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { + match self { + Self::Parse => write!(f, "Failed to parse Config from string"), + Self::StringParse => write!(f, "Failed to parse environment variable"), + Self::Serialize => write!(f, "Failed to serialize Config to TOML"), + Self::IO => write!(f, "Faild to write file"), + } + } +} + +impl From for ConfigError { + fn from(_: toml::ser::Error) -> Self { + ConfigError::Serialize + } +} + +impl From for ConfigError { + fn from(_: std::io::Error) -> Self { + ConfigError::IO + } +} + +impl From for ConfigError { + fn from(_: std::num::ParseIntError) -> Self { + ConfigError::StringParse } } diff --git a/src/db/errors.rs b/src/db/errors.rs index ab7308e..499e7b7 100644 --- a/src/db/errors.rs +++ b/src/db/errors.rs @@ -1,63 +1,48 @@ use deadpool_diesel::postgres::PoolError; +use deadpool_sync::InteractError; +use diesel::result::Error as DieselError; +use std::error::Error as StdError; +use std::fmt::Display; -#[derive(Debug)] -pub enum DatabaseError { - Connection(E), - Interaction, - Operation(E), +#[derive(Debug, utoipa::ToSchema)] +pub enum DatabaseError { + Connection, + Interaction(InteractError), + Operation(DieselError), + Query(DieselError), Migration, + Internal, } -impl std::fmt::Display for DatabaseError { +impl StdError for DatabaseError {} + +impl Display for DatabaseError { fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { match self { - Self::Connection(e) => write!(f, "Failed to connect to database: {}", e), - Self::Interaction => write!(f, "Failed to interact with database"), - Self::Operation(e) => write!(f, "Failed operation: {}", e), + Self::Connection => write!(f, "Failed pool connection"), + Self::Interaction(ref e) => e.fmt(f), + Self::Operation(ref e) => e.fmt(f), + Self::Query(ref e) => e.fmt(f), Self::Migration => write!(f, "Failed to run migrations"), + Self::Internal => write!(f, "Internal error ..."), } } } -impl std::error::Error for DatabaseError { - fn source(&self) -> Option<&(dyn std::error::Error + 'static)> { - match self { - Self::Interaction | Self::Migration => None, - Self::Connection(e) => Some(e), - Self::Operation(e) => Some(e), - } +impl From for DatabaseError { + fn from(_: PoolError) -> Self { + Self::Connection } } -impl From for DatabaseError { - fn from(e: PoolError) -> Self { - Self::Connection(e) +impl From for DatabaseError { + fn from(e: InteractError) -> Self { + Self::Interaction(e) } } -#[derive(Debug)] -pub enum UserError { - Query, - Exists, - NotFound, - HashPassword, -} - -impl std::fmt::Display for UserError { - fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { - match self { - Self::Query => write!(f, "User query failed"), - UserError::Exists => write!(f, "User already exists"), - UserError::NotFound => write!(f, "User not found"), - UserError::HashPassword => write!(f, "Failed to hash user password"), - } - } -} - -impl std::error::Error for UserError {} - -impl From for DatabaseError { - fn from(e: UserError) -> Self { - Self::Operation(e) +impl From for DatabaseError { + fn from(e: DieselError) -> Self { + Self::Query(e) } } diff --git a/src/db/migrations/2024-03-04-191840_create_users/up.sql b/src/db/migrations/2024-03-04-191840_create_users/up.sql index 5502103..fea5a0a 100644 --- a/src/db/migrations/2024-03-04-191840_create_users/up.sql +++ b/src/db/migrations/2024-03-04-191840_create_users/up.sql @@ -7,5 +7,6 @@ CREATE TABLE "users"( "hashed_password" TEXT NOT NULL, "name" TEXT NOT NULL, "email" TEXT NOT NULL, - "is_admin" BOOL NOT NULL + "is_admin" BOOL NOT NULL, + "avatar" TEXT NOT NULL ); diff --git a/src/db/mod.rs b/src/db/mod.rs index 091e46c..1a506b7 100644 --- a/src/db/mod.rs +++ b/src/db/mod.rs @@ -4,6 +4,7 @@ pub mod user; use deadpool_diesel::postgres::Manager; pub use deadpool_diesel::postgres::Pool; +use diesel::prelude::*; use diesel_migrations::{embed_migrations, EmbeddedMigrations, MigrationHarness}; use errors::DatabaseError; @@ -16,12 +17,26 @@ pub fn create_pool(database_url: String) -> Pool { Pool::builder(manager).build().unwrap() } -pub async fn run_migrations(pool: &Pool) -> Result<(), DatabaseError> { - let connection = pool.get().await.map_err(DatabaseError::Connection)?; +pub async fn execute(pool: &Pool, f: F) -> Result +where + F: FnOnce(&mut PgConnection) -> Result + Send + 'static, + T: Send + 'static, +{ + let connection = pool.get().await.map_err(|_| DatabaseError::Connection)?; + connection - .interact(move |connection| connection.run_pending_migrations(MIGRATIONS).map(|_| ())) + .interact(move |connection| f(connection)) .await - .map_err(|_| DatabaseError::Interaction)? - .map_err(|_| DatabaseError::Migration)?; - Ok(()) + .map_err(DatabaseError::Interaction)? + .map_err(DatabaseError::Query) +} + +pub async fn run_migrations(pool: &Pool) -> Result<(), DatabaseError> { + execute(pool, move |connection| { + Ok(connection + .run_pending_migrations(MIGRATIONS) + .map(|_| ()) + .map_err(|_| DatabaseError::Migration)) + }) + .await? } diff --git a/src/db/schema.rs b/src/db/schema.rs index fb1bf17..d068126 100644 --- a/src/db/schema.rs +++ b/src/db/schema.rs @@ -8,5 +8,6 @@ diesel::table! { name -> Text, email -> Text, is_admin -> Bool, + avatar -> Text, } } diff --git a/src/db/user.rs b/src/db/user.rs index 43cb7d7..75d7d62 100644 --- a/src/db/user.rs +++ b/src/db/user.rs @@ -1,13 +1,11 @@ -use crate::db::{errors::*, schema::users, Pool}; -use argon2::{password_hash::SaltString, Argon2, PasswordHasher}; +use crate::db::schema::users; use diesel::{ dsl::{AsSelect, SqlTypeOf}, pg::Pg, prelude::*, }; -use rand_core::OsRng; -#[derive(serde::Serialize, Queryable, Selectable, Clone)] +#[derive(serde::Serialize, Queryable, Selectable, Clone, Identifiable, AsChangeset)] #[diesel(table_name = users)] #[diesel(check_for_backend(diesel::pg::Pg))] pub struct User { @@ -17,6 +15,7 @@ pub struct User { pub name: String, pub email: String, pub is_admin: bool, + pub avatar: String, } #[derive(serde::Deserialize, Insertable)] @@ -27,115 +26,11 @@ pub struct NewUser { pub name: String, pub email: String, pub is_admin: bool, + pub avatar: String, } +#[allow(dead_code)] type SqlType = SqlTypeOf>; + +#[allow(dead_code)] type BoxedQuery<'a> = users::BoxedQuery<'a, Pg, SqlType>; - -impl User { - pub async fn register( - pool: &Pool, - login: String, - password: String, - name: String, - email: String, - is_admin: bool, - ) -> Result> { - let user = User::find(pool, User::by_email(email.to_owned())) - .await - .map_err(|_| UserError::Query)?; - - if user.is_some() { - return Err(DatabaseError::Operation(UserError::Exists)); - } - - let hashed_password = Argon2::default() - .hash_password(password.as_bytes(), &SaltString::generate(&mut OsRng)) - .map_err(|_| UserError::HashPassword) - .map(|hash| hash.to_string())?; - - let new_user = NewUser { - login, - hashed_password, - name, - email, - is_admin, - }; - - let user = User::create(pool, new_user) - .await - .map_err(|_| UserError::Query)?; - - Ok(user) - } - - pub async fn create( - pool: &Pool, - new_user: NewUser, - ) -> Result> { - let connection = pool.get().await.map_err(DatabaseError::Connection)?; - let user = connection - .interact(move |connection| { - diesel::insert_into(users::table) - .values(new_user) - .returning(User::as_returning()) - .get_result(connection) - }) - .await - .map_err(|_| DatabaseError::Interaction)? - .map_err(|_| DatabaseError::Interaction)?; - - Ok(user) - } - - pub fn by_email(email: String) -> BoxedQuery<'static> { - users::table - .into_boxed() - .select(User::as_select()) - .filter(users::email.eq(email)) - } - - pub fn by_id(id: uuid::Uuid) -> BoxedQuery<'static> { - users::table - .into_boxed() - .select(User::as_select()) - .filter(users::id.eq(id)) - } - - pub fn by_login(login: String) -> BoxedQuery<'static> { - users::table - .into_boxed() - .select(User::as_select()) - .filter(users::login.eq(login)) - } - - pub async fn find( - pool: &Pool, - query: BoxedQuery<'static>, - ) -> Result, DatabaseError> { - let connection = pool.get().await.map_err(DatabaseError::Connection)?; - let user = connection - .interact(move |connection| query.first(connection).optional()) - .await - .map_err(|_| DatabaseError::Interaction)? - .map_err(|_| DatabaseError::Interaction)?; - - Ok(user) - } - - pub async fn remove( - pool: &Pool, - user: User, - ) -> Result<(), DatabaseError> { - let connection = pool.get().await.map_err(DatabaseError::Connection)?; - connection - .interact(move |connection| { - diesel::delete(users::table.filter(users::id.eq(user.id))).execute(connection) - }) - .await - .map_err(|_| DatabaseError::Interaction)? - .map_err(|_| DatabaseError::Interaction)?; - - Ok(()) - } -} diff --git a/src/error_handle.rs b/src/error_handle.rs deleted file mode 100644 index eee86f9..0000000 --- a/src/error_handle.rs +++ /dev/null @@ -1,25 +0,0 @@ -use axum::{http::StatusCode, Json}; -use tracing_subscriber::{layer::SubscriberExt, util::SubscriberInitExt}; - -pub fn init_tracing() { - tracing_subscriber::registry() - .with( - tracing_subscriber::EnvFilter::try_from_default_env() - .unwrap_or_else(|_| "elnafo=debug".into()), - ) - .with(tracing_subscriber::fmt::layer()) - .init(); -} - -pub fn internal_error(err: E) -> (StatusCode, Json) -where - E: std::error::Error, -{ - ( - StatusCode::INTERNAL_SERVER_ERROR, - Json(serde_json::json!({ - "status": "fail", - "message": err.to_string() - })), - ) -} diff --git a/src/main.rs b/src/main.rs index 1d227d4..f7e92c2 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,112 +1,68 @@ pub mod api; pub mod config; pub mod db; -pub mod error_handle; +pub mod resources; pub mod state; -use axum::{ - extract::Path, - http::{header::CONTENT_TYPE, StatusCode, Uri}, - response::{IntoResponse, Response}, - routing::get, - Router, -}; +use axum::{http::Uri, response::IntoResponse, routing::get, Router}; use std::net::SocketAddr; use std::sync::Arc; use tower_http::trace::{self, TraceLayer}; use tracing::Level; +use utoipa::OpenApi; +use utoipa_rapidoc::RapiDoc; use crate::config::Config; -use crate::error_handle::*; use crate::state::AppState; #[tokio::main] -async fn main() { - //init_tracing(); +async fn main() -> Result<(), Box> { tracing_subscriber::fmt() .with_target(false) .compact() .init(); - let config = Config::new(); - let database_url = format!( - "postgres://{}:{}@{}:{}/{}", - config.database.user, - config.database.password, - config.database.host, - config.database.port, - config.database.name - ); + let config = match Config::open(Config::data_dir()?.join("config.toml").as_path()) { + Ok(config) => config, + Err(_) => Config::new(), + }; - let pool = db::create_pool(database_url); + let pool = db::create_pool(config.database_url()); - db::run_migrations(&pool).await; + db::run_migrations(&pool).await?; let state = Arc::new(AppState { database: pool.clone(), config: config.clone(), }); - let address: SocketAddr = format!("{}:{}", config.server.address, config.server.port) - .parse() - .unwrap(); //SocketAddr::from((Ipv4Addr::UNSPECIFIED, 54600)); - - let lister = tokio::net::TcpListener::bind(&address).await.unwrap(); - let app = Router::new() - .route("/", get(home)) - .route("/user/login", get(user_login)) - .route("/assets/*file", get(static_handler)) - .nest("/api", api::v1::routes(state)) + .nest("/resources", resources::routes(state.clone())) + .nest("/api", api::routes(state)) + .merge( + RapiDoc::with_openapi("/api/openapi.json", api::doc::ApiDoc::openapi()) + .path("/api/rapidoc"), + ) + .route("/", get(frontend_handler)) + .route("/*frontend", get(frontend_handler)) .layer( TraceLayer::new_for_http() .make_span_with(trace::DefaultMakeSpan::new().level(Level::INFO)) .on_response(trace::DefaultOnResponse::new().level(Level::INFO)), ); - println!("listening on http://{}", address); + let address: SocketAddr = + format!("{}:{}", config.server.address, config.server.port).parse()?; - axum::serve(lister, app.into_make_service()) - .await - .map_err(internal_error) - .unwrap(); + let lister = tokio::net::TcpListener::bind(&address).await?; + + println!("listening on {}", address); + + axum::serve(lister, app.into_make_service()).await?; + + Ok(()) } -async fn home() -> impl IntoResponse { - frontend::BaseTemplate { view: "app" } -} - -async fn user_login() -> impl IntoResponse { - frontend::BaseTemplate { view: "app" } -} - -async fn user(Path(user): Path) -> impl IntoResponse {} - -async fn static_handler(uri: Uri) -> impl IntoResponse { - let mut path = uri.path().trim_start_matches('/').to_string(); - - if path.starts_with("assets/") { - path = path.replace("assets/", ""); - } - - StaticFile(path) -} - -pub struct StaticFile(pub T); - -impl IntoResponse for StaticFile -where - T: Into, -{ - fn into_response(self) -> Response { - let path = self.0.into(); - - match frontend::Assets::get(path.as_str()) { - Some(content) => { - let mime = mime_guess::from_path(path).first_or_octet_stream(); - ([(CONTENT_TYPE, mime.as_ref())], content.data).into_response() - } - None => (StatusCode::NOT_FOUND, "404 Not Found").into_response(), - } - } +async fn frontend_handler(_: Uri) -> impl IntoResponse { + elnafo_frontend::BaseTemplate { view: "app" } } diff --git a/src/resources/mod.rs b/src/resources/mod.rs new file mode 100644 index 0000000..46e89c4 --- /dev/null +++ b/src/resources/mod.rs @@ -0,0 +1,104 @@ +use std::sync::Arc; + +use axum::{ + http::{ + header::{self, ACCEPT_ENCODING, CONTENT_TYPE, ORIGIN}, + Method, StatusCode, Uri, + }, + response::{IntoResponse, Response}, + routing::get, + Router, +}; +use tower_http::{ + compression::{CompressionLayer, DefaultPredicate}, + cors::CorsLayer, +}; + +use crate::{config::Config, state::AppState}; + +pub fn routes(state: Arc) -> Router { + let cors = CorsLayer::new() + .allow_methods([Method::GET]) + .allow_headers(vec![ORIGIN, CONTENT_TYPE, ACCEPT_ENCODING]) + .allow_origin([ + "http://localhost:54600".parse().unwrap(), + "http://localhost:5173".parse().unwrap(), + ]) + .allow_credentials(true); + + let compression = CompressionLayer::new().gzip(true); + + Router::new() + .route("/assets/*file", get(assets)) + .route("/avatars/*avatar_id", get(avatars).layer(compression)) + .layer(cors) +} + +async fn assets(uri: Uri) -> Result { + let path = uri.path().trim_start_matches("/assets/").to_string(); + + match elnafo_frontend::Assets::get(&path) { + Some(content) => { + let mime = mime_guess::from_path(path).first_or_octet_stream(); + Ok(([(header::CONTENT_TYPE, mime.as_ref())], content.data).into_response()) + } + None => Err(ResourceError::NotFound), + } +} + +async fn avatars(uri: Uri) -> Result { + let avatar_id = uri.path().trim_start_matches("/avatars/").to_string(); + let path = Config::data_dir().unwrap().join("avatars").join(avatar_id); + + let reader = image::io::Reader::open(path.clone()) + .map_err(|_| ResourceError::NotFound)? + .with_guessed_format() + .map_err(|_| ResourceError::BadFormat)?; + let format = reader.format(); + + let mime = format.map_or("application/octet-stream", |f| f.to_mime_type()); + let content = reader.decode().map_err(|_| ResourceError::BadContent)?; + + let mut bytes: Vec = Vec::new(); + let _ = match format { + Some(format) => content + .write_to(&mut std::io::Cursor::new(&mut bytes), format) + .map_err(|_| ResourceError::BadContent), + None => return Err(ResourceError::BadFormat), + }; + + Ok(([(header::CONTENT_TYPE, mime)], bytes).into_response()) +} + +#[derive(Debug)] +pub enum ResourceError { + NotFound, + NotExists, + BadFormat, + BadContent, +} + +impl std::error::Error for ResourceError {} + +impl std::fmt::Display for ResourceError { + fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { + match self { + Self::NotFound => write!(f, "Resource was not found"), + Self::NotExists => write!(f, "Resource was not found"), + Self::BadFormat => write!(f, "Cannot determine file format"), + Self::BadContent => write!(f, "Failed to read a file content"), + } + } +} + +impl IntoResponse for ResourceError { + fn into_response(self) -> Response { + let status = match self { + Self::NotFound => StatusCode::NOT_FOUND, + Self::NotExists => StatusCode::NO_CONTENT, + Self::BadFormat | Self::BadContent => StatusCode::INTERNAL_SERVER_ERROR, + }; + + (status, format!("{}", self)).into_response() + } +}