From fe80fefba1215a7c8ac9486b2cd5bef8c6ace2cd Mon Sep 17 00:00:00 2001 From: L-Nafaryus Date: Mon, 11 Mar 2024 22:37:59 +0500 Subject: [PATCH] user register, login, logout, jwt middleware auth --- Cargo.lock | 488 ++++++++++++++++++ Cargo.toml | 14 +- src/api/mod.rs | 1 + src/api/v1/mod.rs | 302 ++++++++++- .../2024-03-04-191840_create_users/up.sql | 4 +- src/db/mod.rs | 8 +- src/db/models.rs | 55 +- src/db/schema.rs | 2 +- src/error_handle.rs | 25 + src/main.rs | 66 +-- src/state.rs | 6 + 11 files changed, 917 insertions(+), 54 deletions(-) create mode 100644 src/api/mod.rs create mode 100644 src/error_handle.rs create mode 100644 src/state.rs diff --git a/Cargo.lock b/Cargo.lock index 9c8035c..6175caa 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -26,6 +26,33 @@ dependencies = [ "memchr", ] +[[package]] +name = "android-tzdata" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e999941b234f3131b00bc13c22d06e8c5ff726d1b6318ac7eb276997bbb4fef0" + +[[package]] +name = "android_system_properties" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311" +dependencies = [ + "libc", +] + +[[package]] +name = "argon2" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c3610892ee6e0cbce8ae2700349fcf8f98adb0dbfbee85aec3c9179d29cc072" +dependencies = [ + "base64ct", + "blake2", + "cpufeatures", + "password-hash", +] + [[package]] name = "async-trait" version = "0.1.77" @@ -98,6 +125,28 @@ dependencies = [ "tracing", ] +[[package]] +name = "axum-extra" +version = "0.9.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "895ff42f72016617773af68fb90da2a9677d89c62338ec09162d4909d86fdd8f" +dependencies = [ + "axum", + "axum-core", + "bytes", + "cookie", + "futures-util", + "http", + "http-body", + "http-body-util", + "mime", + "pin-project-lite", + "serde", + "tower", + "tower-layer", + "tower-service", +] + [[package]] name = "backtrace" version = "0.3.69" @@ -113,12 +162,48 @@ dependencies = [ "rustc-demangle", ] +[[package]] +name = "base64" +version = "0.21.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d297deb1925b89f2ccc13d7635fa0714f12c87adce1c75356b39ca9b7178567" + +[[package]] +name = "base64ct" +version = "1.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8c3c1a368f70d6cf7302d78f8f7093da241fb8e8807c05cc9e51a125895a6d5b" + [[package]] name = "bitflags" version = "2.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ed570934406eb16438a4e976b1b4500774099c13b8cb96eec99f620f05090ddf" +[[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" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" +dependencies = [ + "generic-array", +] + +[[package]] +name = "bumpalo" +version = "3.15.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ea184aa71bb362a1157c896979544cc23974e08fd265f29ea96b59f0b4a555b" + [[package]] name = "byteorder" version = "1.5.0" @@ -143,6 +228,57 @@ version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" +[[package]] +name = "chrono" +version = "0.4.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8eaf5903dcbc0a39312feb77df2ff4c76387d591b9fc7b04a238dcf8bb62639a" +dependencies = [ + "android-tzdata", + "iana-time-zone", + "js-sys", + "num-traits", + "serde", + "wasm-bindgen", + "windows-targets 0.52.4", +] + +[[package]] +name = "cookie" +version = "0.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3cd91cf61412820176e137621345ee43b3f4423e589e7ae4e50d601d93e35ef8" +dependencies = [ + "percent-encoding", + "time", + "version_check", +] + +[[package]] +name = "core-foundation-sys" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06ea2b9bc92be3c2baa9334a323ebca2d6f074ff852cd1d7b11064035cd3868f" + +[[package]] +name = "cpufeatures" +version = "0.2.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53fe5e26ff1b7aef8bca9c6080520cfb8d9333c7568e1829cef191a9723e5504" +dependencies = [ + "libc", +] + +[[package]] +name = "crypto-common" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3" +dependencies = [ + "generic-array", + "typenum", +] + [[package]] name = "deadpool" version = "0.10.0" @@ -184,6 +320,15 @@ dependencies = [ "deadpool-runtime", ] +[[package]] +name = "deranged" +version = "0.3.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b42b6fa04a440b495c8b04d0e71b707c585f83cb9cb28cf8cd0d976c315e31b4" +dependencies = [ + "powerfmt", +] + [[package]] name = "diesel" version = "2.1.4" @@ -192,9 +337,12 @@ checksum = "62c6fcf842f17f8c78ecf7c81d75c5ce84436b41ee07e03f490fbb5f5a8731d8" dependencies = [ "bitflags", "byteorder", + "chrono", "diesel_derives", "itoa", "pq-sys", + "time", + "uuid", ] [[package]] @@ -229,6 +377,17 @@ dependencies = [ "syn", ] +[[package]] +name = "digest" +version = "0.10.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" +dependencies = [ + "block-buffer", + "crypto-common", + "subtle", +] + [[package]] name = "dotenvy" version = "0.15.7" @@ -239,16 +398,23 @@ checksum = "1aaf95b3e5c8f23aa320147307562d361db0ae0d51242340f558153b4eb2439b" name = "elnafo" version = "0.1.0" dependencies = [ + "argon2", "axum", + "axum-extra", + "chrono", "deadpool-diesel", "diesel", "diesel_migrations", "dotenvy", + "jsonwebtoken", + "rand_core", "serde", "serde_json", + "time", "tokio", "tracing", "tracing-subscriber", + "uuid", ] [[package]] @@ -311,6 +477,29 @@ dependencies = [ "pin-utils", ] +[[package]] +name = "generic-array" +version = "0.14.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" +dependencies = [ + "typenum", + "version_check", +] + +[[package]] +name = "getrandom" +version = "0.2.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "190092ea657667030ac6a35e305e62fc4dd69fd98ac98631e5d3a2b1575a12b5" +dependencies = [ + "cfg-if", + "js-sys", + "libc", + "wasi", + "wasm-bindgen", +] + [[package]] name = "gimli" version = "0.28.1" @@ -430,6 +619,29 @@ dependencies = [ "tokio", ] +[[package]] +name = "iana-time-zone" +version = "0.1.60" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7ffbb5a1b541ea2561f8c41c087286cc091e21e556a4f09a8f6cbf17b69b141" +dependencies = [ + "android_system_properties", + "core-foundation-sys", + "iana-time-zone-haiku", + "js-sys", + "wasm-bindgen", + "windows-core", +] + +[[package]] +name = "iana-time-zone-haiku" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f" +dependencies = [ + "cc", +] + [[package]] name = "indexmap" version = "2.2.5" @@ -446,6 +658,30 @@ version = "1.0.10" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b1a46d1a171d865aa5f83f92695765caa047a9b4cbae2cbf37dbd613a793fd4c" +[[package]] +name = "js-sys" +version = "0.3.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29c15563dc2726973df627357ce0c9ddddbea194836909d655df6a75d2cf296d" +dependencies = [ + "wasm-bindgen", +] + +[[package]] +name = "jsonwebtoken" +version = "9.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c7ea04a7c5c055c175f189b6dc6ba036fd62306b58c66c9f6389036c503a3f4" +dependencies = [ + "base64", + "js-sys", + "pem", + "ring", + "serde", + "serde_json", + "simple_asn1", +] + [[package]] name = "lazy_static" version = "1.4.0" @@ -542,6 +778,41 @@ dependencies = [ "winapi", ] +[[package]] +name = "num-bigint" +version = "0.4.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "608e7659b5c3d7cba262d894801b9ec9d00de989e8a82bd4bef91d08da45cdc0" +dependencies = [ + "autocfg", + "num-integer", + "num-traits", +] + +[[package]] +name = "num-conv" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "51d515d32fb182ee37cda2ccdcb92950d6a3c2893aa280e540671c2cd0f3b1d9" + +[[package]] +name = "num-integer" +version = "0.1.46" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7969661fd2958a5cb096e56c8e1ad0444ac2bbcd0061bd28660485a44879858f" +dependencies = [ + "num-traits", +] + +[[package]] +name = "num-traits" +version = "0.2.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da0df0e5185db44f69b44f26786fe401b6c293d1907744beaa7fa62b2e5a517a" +dependencies = [ + "autocfg", +] + [[package]] name = "num_cpus" version = "1.16.0" @@ -573,6 +844,27 @@ version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b15813163c1d831bf4a13c3610c05c0d03b39feb07f7e09fa234dac9b15aaf39" +[[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 = "pem" +version = "3.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b8fcc794035347fb64beda2d3b462595dd2753e3f268d89c5aae77e8cf2c310" +dependencies = [ + "base64", + "serde", +] + [[package]] name = "percent-encoding" version = "2.3.1" @@ -611,6 +903,12 @@ version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" +[[package]] +name = "powerfmt" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" + [[package]] name = "pq-sys" version = "0.4.8" @@ -638,6 +936,15 @@ dependencies = [ "proc-macro2", ] +[[package]] +name = "rand_core" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" +dependencies = [ + "getrandom", +] + [[package]] name = "regex" version = "1.10.3" @@ -682,6 +989,21 @@ version = "0.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c08c74e62047bb2de4ff487b251e4a92e24f48745648451635cec7d591162d9f" +[[package]] +name = "ring" +version = "0.17.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c17fa4cb658e3583423e915b9f3acc01cceaee1860e33d59ebae66adc3a2dc0d" +dependencies = [ + "cc", + "cfg-if", + "getrandom", + "libc", + "spin", + "untrusted", + "windows-sys 0.52.0", +] + [[package]] name = "rustc-demangle" version = "0.1.23" @@ -771,6 +1093,18 @@ dependencies = [ "lazy_static", ] +[[package]] +name = "simple_asn1" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "adc4e5204eb1910f40f9cfa375f6f05b68c3abac4b6fd879c8ff5e7ae8a0a085" +dependencies = [ + "num-bigint", + "num-traits", + "thiserror", + "time", +] + [[package]] name = "slab" version = "0.4.9" @@ -796,6 +1130,18 @@ dependencies = [ "windows-sys 0.52.0", ] +[[package]] +name = "spin" +version = "0.9.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6980e8d7511241f8acf4aebddbb1ff938df5eebe98691418c4468d0b72a96a67" + +[[package]] +name = "subtle" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "81cdd64d312baedb58e21336b31bc043b77e01cc99033ce76ef539f78e965ebc" + [[package]] name = "syn" version = "2.0.52" @@ -813,6 +1159,26 @@ version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2047c6ded9c721764247e62cd3b03c09ffc529b2ba5b10ec482ae507a4a70160" +[[package]] +name = "thiserror" +version = "1.0.57" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e45bcbe8ed29775f228095caf2cd67af7a4ccf756ebff23a306bf3e8b47b24b" +dependencies = [ + "thiserror-impl", +] + +[[package]] +name = "thiserror-impl" +version = "1.0.57" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a953cb265bef375dae3de6663da4d3804eee9682ea80d8e2542529b73c531c81" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "thread_local" version = "1.1.8" @@ -823,6 +1189,37 @@ dependencies = [ "once_cell", ] +[[package]] +name = "time" +version = "0.3.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8248b6521bb14bc45b4067159b9b6ad792e2d6d754d6c41fb50e29fefe38749" +dependencies = [ + "deranged", + "itoa", + "num-conv", + "powerfmt", + "serde", + "time-core", + "time-macros", +] + +[[package]] +name = "time-core" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef927ca75afb808a4d64dd374f00a2adf8d0fcff8e7b184af886c3c87ec4a3f3" + +[[package]] +name = "time-macros" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7ba3a3ef41e6672a2f0f001392bb5dcd3ff0a9992d618ca761a11c3121547774" +dependencies = [ + "num-conv", + "time-core", +] + [[package]] name = "tokio" version = "1.36.0" @@ -989,12 +1386,34 @@ dependencies = [ "tracing-log", ] +[[package]] +name = "typenum" +version = "1.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42ff0bf0c66b8238c6f3b578df37d0b7848e55df8577b3f74f92a69acceeb825" + [[package]] name = "unicode-ident" version = "1.0.12" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b" +[[package]] +name = "untrusted" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" + +[[package]] +name = "uuid" +version = "1.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f00cc9702ca12d3c81455259621e676d0f7251cec66a21e98fe2e9a37db93b2a" +dependencies = [ + "getrandom", + "serde", +] + [[package]] name = "valuable" version = "0.1.0" @@ -1007,12 +1426,72 @@ version = "0.2.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" +[[package]] +name = "version_check" +version = "0.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f" + [[package]] name = "wasi" version = "0.11.0+wasi-snapshot-preview1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" +[[package]] +name = "wasm-bindgen" +version = "0.2.92" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4be2531df63900aeb2bca0daaaddec08491ee64ceecbee5076636a3b026795a8" +dependencies = [ + "cfg-if", + "wasm-bindgen-macro", +] + +[[package]] +name = "wasm-bindgen-backend" +version = "0.2.92" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "614d787b966d3989fa7bb98a654e369c762374fd3213d212cfc0251257e747da" +dependencies = [ + "bumpalo", + "log", + "once_cell", + "proc-macro2", + "quote", + "syn", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.92" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1f8823de937b71b9460c0c34e25f3da88250760bec0ebac694b49997550d726" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.92" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e94f17b526d0a461a191c78ea52bbce64071ed5c04c9ffe424dcb38f74171bb7" +dependencies = [ + "proc-macro2", + "quote", + "syn", + "wasm-bindgen-backend", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.92" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "af190c94f2773fdb3729c55b007a722abb5384da03bc0986df4c289bf5567e96" + [[package]] name = "winapi" version = "0.3.9" @@ -1035,6 +1514,15 @@ version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" +[[package]] +name = "windows-core" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "33ab640c8d7e35bf8ba19b884ba838ceb4fba93a4e8c65a9059d08afcfc683d9" +dependencies = [ + "windows-targets 0.52.4", +] + [[package]] name = "windows-sys" version = "0.48.0" diff --git a/Cargo.toml b/Cargo.toml index 719f85b..1c21092 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -14,8 +14,20 @@ tokio = { version = "1.36.0", default-features = false, features = [ dotenvy = "0.15.7" tracing = "0.1.40" tracing-subscriber = { version = "0.3.18", features = ["env-filter"] } -diesel = { version = "2.1.4", features = ["postgres"] } +diesel = { version = "2.1.4", features = [ + "postgres", + "chrono", + "uuid", + "time", +] } deadpool-diesel = { version = "0.5.0", features = ["postgres"] } diesel_migrations = "2.1.0" serde = { version = "1.0.197", features = ["derive"] } serde_json = "1.0.114" +uuid = { version = "1.7.0", features = ["serde", "v4"] } +time = "0.3.34" +argon2 = "0.5.3" +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"] } diff --git a/src/api/mod.rs b/src/api/mod.rs new file mode 100644 index 0000000..a3a6d96 --- /dev/null +++ b/src/api/mod.rs @@ -0,0 +1 @@ +pub mod v1; diff --git a/src/api/v1/mod.rs b/src/api/v1/mod.rs index 7d12d9a..b9a7663 100644 --- a/src/api/v1/mod.rs +++ b/src/api/v1/mod.rs @@ -1,14 +1,298 @@ -pub fn add(left: usize, right: usize) -> usize { - left + right +use crate::db::models::{FilteredUser, LoginUser, NewUser, RegisterUser, TokenClaims, User}; +use crate::error_handle::internal_error; +use crate::state::AppState; +use argon2::{password_hash::SaltString, Argon2}; +use argon2::{PasswordHash, PasswordHasher, PasswordVerifier}; +use axum::extract::Request; +use axum::Extension; +use axum::{ + body::Body, + extract::State, + http::{header, StatusCode}, + middleware::Next, + response::{IntoResponse, Response}, + Json, +}; +use axum_extra::extract::cookie::{self, Cookie, SameSite}; +use axum_extra::extract::CookieJar; +use diesel::{connection, prelude::*}; +use jsonwebtoken::{decode, encode, DecodingKey, EncodingKey, Header, Validation}; +use rand_core::OsRng; +use std::sync::Arc; + +pub async fn healthcheck() -> impl IntoResponse { + Json(serde_json::json!({ + "status": "success", + "message": "healthy" + })) } -#[cfg(test)] -mod tests { - use super::*; +pub async fn register_user( + State(state): State>, + Json(body): Json, +) -> Result)> { + use crate::db::schema::{users, users::dsl}; - #[test] - fn it_works() { - let result = add(2, 2); - assert_eq!(result, 4); + let connection = state.database.get().await.unwrap(); + let (login, email) = (body.login.clone(), body.email.clone()); + let user_exists = connection + .interact(move |connection| { + dsl::users + .filter(dsl::login.eq(login).or(dsl::email.eq(email))) + .select(User::as_select()) + .first(connection) + .optional() + }) + .await + .map_err(internal_error)? + .map_err(|e| { + ( + StatusCode::INTERNAL_SERVER_ERROR, + Json(serde_json::json!({ + "status": "fail", + "message": format!("Database error: {}", e) + })), + ) + })?; + + if user_exists.is_some() { + return Err(( + StatusCode::CONFLICT, + Json(serde_json::json!({ + "status": "fail", + "message": "Login or email already exists" + })), + )); } + + let salt = SaltString::generate(&mut OsRng); + let hashed_password = Argon2::default() + .hash_password(body.password.as_bytes(), &salt) + .map_err(|e| { + ( + StatusCode::INTERNAL_SERVER_ERROR, + Json(serde_json::json!({ + "status": "fail", + "message": format!("Error while hashing password: {}", e) + })), + ) + }) + .map(|hash| hash.to_string())?; + + let user = NewUser { + login: body.login.to_string(), + hashed_password: hashed_password, + name: body.name, + email: body.email, + is_admin: body.is_admin, + }; + + let new_user = connection + .interact(move |connection| { + diesel::insert_into(users::table) + .values(&user) + .returning(User::as_returning()) + .get_result(connection) + }) + .await + .map_err(internal_error)? + .map_err(|e| { + ( + StatusCode::INTERNAL_SERVER_ERROR, + Json(serde_json::json!({ + "status": "fail", + "message": format!("Database error: {}", e) + })), + ) + })?; + + let response = serde_json::json!({"status": "success", "data": serde_json::json!({"user": FilteredUser::from(&new_user)})}); + + Ok(Json(response)) +} + +pub async fn login_user( + State(state): State>, + Json(body): Json, +) -> Result)> { + use crate::db::schema::{users, users::dsl}; + + let connection = state.database.get().await.unwrap(); + let user = connection + .interact(move |connection| { + dsl::users + .filter(dsl::email.eq(body.email)) + .select(User::as_select()) + .first(connection) + }) + .await + .map_err(internal_error)? + .map_err(|e| { + ( + StatusCode::BAD_REQUEST, + Json(serde_json::json!({ + "status": "fail", + "message": format!("Invalid login or email: {}", e) + })), + ) + })?; + + let is_valid = 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, + }; + + if !is_valid { + return Err(( + StatusCode::BAD_REQUEST, + Json(serde_json::json!({ + "status": "fail", + "message": "Invalid login, email or password" + })), + )); + } + + let now = chrono::Utc::now(); + let iat = now.timestamp() as usize; + let exp = (now + chrono::Duration::try_minutes(60).unwrap()).timestamp() as usize; + let claims = TokenClaims { + sub: user.id.to_string(), + exp, + iat, + }; + + let token = encode( + &Header::default(), + &claims, + &EncodingKey::from_secret(state.config.jwt.secret.as_ref()), + ) + .unwrap(); + + let cookie = Cookie::build(("token", token.to_owned())) + .path("/") + .max_age(time::Duration::hours(1)) + .same_site(SameSite::Lax) + .http_only(true); + + let mut response = + Response::new(serde_json::json!({"status": "success", "token": token}).to_string()); + response + .headers_mut() + .insert(header::SET_COOKIE, cookie.to_string().parse().unwrap()); + + Ok(response) +} + +pub async fn logout_user() -> Result)> { + let cookie = Cookie::build(("token", "")) + .path("/") + .max_age(time::Duration::hours(-1)) + .same_site(SameSite::Lax) + .http_only(true); + + let mut response = Response::new(serde_json::json!({"status": "success"}).to_string()); + response + .headers_mut() + .insert(header::SET_COOKIE, cookie.to_string().parse().unwrap()); + + Ok(response) +} + +pub async fn jwt_auth( + cookie_jar: CookieJar, + State(state): State>, + mut req: Request, + next: Next, +) -> Result)> { + let token = cookie_jar + .get("token") + .map(|cookie| cookie.value().to_string()) + .or_else(|| { + req.headers() + .get(header::AUTHORIZATION) + .and_then(|auth_header| auth_header.to_str().ok()) + .and_then(|auth_value| { + if auth_value.starts_with("Bearer ") { + Some(auth_value[7..].to_owned()) + } else { + None + } + }) + }); + + let token = token.ok_or_else(|| { + ( + StatusCode::UNAUTHORIZED, + Json(serde_json::json!({ + "status": "fail", + "message": "Cannot login without token" + })), + ) + })?; + + let claims = decode::( + &token, + &DecodingKey::from_secret(state.config.jwt.secret.as_ref()), + &Validation::default(), + ) + .map_err(|_| { + ( + StatusCode::UNAUTHORIZED, + Json(serde_json::json!({"status":"fail","message":"Invalid token"})), + ) + })? + .claims; + + let user_id = uuid::Uuid::parse_str(&claims.sub).map_err(|_| { + ( + StatusCode::UNAUTHORIZED, + Json(serde_json::json!({"status":"fail","message":"Invalid token"})), + ) + })?; + + use crate::db::schema::{users, users::dsl}; + + let connection = state.database.get().await.unwrap(); + let user = connection + .interact(move |connection| { + dsl::users + .filter(dsl::id.eq(user_id)) + .select(User::as_select()) + .first(connection) + .optional() + }) + .await + .map_err(internal_error)? + .map_err(|e| { + ( + StatusCode::INTERNAL_SERVER_ERROR, + Json(serde_json::json!({ + "status": "fail", + "message": format!("Database error: {}", e) + })), + ) + })?; + + let user = user.ok_or_else(|| { + ( + StatusCode::UNAUTHORIZED, + Json(serde_json::json!({ + "status": "fail", + "message": "The user belonging to this token no longer exists" + })), + ) + })?; + + req.extensions_mut().insert(user); + Ok(next.run(req).await) +} + +pub async fn me( + Extension(user): Extension, +) -> Result)> { + Ok(Json( + serde_json::json!({"status":"success","data":serde_json::json!({"user":FilteredUser::from(&user)})}), + )) } 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 93b2f27..5502103 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 @@ -1,6 +1,8 @@ -- Your SQL goes here +CREATE EXTENSION IF NOT EXISTS "uuid-ossp"; + CREATE TABLE "users"( - "id" SERIAL NOT NULL PRIMARY KEY, + "id" UUID NOT NULL PRIMARY KEY DEFAULT (uuid_generate_v4()), "login" TEXT NOT NULL, "hashed_password" TEXT NOT NULL, "name" TEXT NOT NULL, diff --git a/src/db/mod.rs b/src/db/mod.rs index 2a4154b..545a402 100644 --- a/src/db/mod.rs +++ b/src/db/mod.rs @@ -26,10 +26,10 @@ pub async fn run_migrations(pool: &Pool) { pub fn create_user( connection: &mut PgConnection, - login: &str, - hashed_password: &str, - name: &str, - email: &str, + login: String, + hashed_password: String, + name: String, + email: String, is_admin: bool, ) -> User { use crate::db::schema::users; diff --git a/src/db/models.rs b/src/db/models.rs index beb1fc3..8cc3df4 100644 --- a/src/db/models.rs +++ b/src/db/models.rs @@ -1,11 +1,11 @@ use crate::db::schema; use diesel::prelude::*; -#[derive(serde::Serialize, Queryable, Selectable)] +#[derive(serde::Serialize, Queryable, Selectable, Clone)] #[diesel(table_name = schema::users)] #[diesel(check_for_backend(diesel::pg::Pg))] pub struct User { - pub id: i32, + pub id: uuid::Uuid, pub login: String, pub hashed_password: String, pub name: String, @@ -15,10 +15,51 @@ pub struct User { #[derive(serde::Deserialize, Insertable)] #[diesel(table_name = schema::users)] -pub struct NewUser<'a> { - pub login: &'a str, - pub hashed_password: &'a str, - pub name: &'a str, - pub email: &'a str, +pub struct NewUser { + pub login: String, + pub hashed_password: String, + pub name: String, + pub email: String, pub is_admin: bool, } + +#[derive(serde::Deserialize)] +pub struct RegisterUser { + pub login: String, + pub password: String, + pub name: String, + pub email: String, + pub is_admin: bool, +} + +#[derive(serde::Serialize)] +pub struct FilteredUser { + pub id: String, + pub name: String, + pub email: String, + pub is_admin: bool, +} + +impl FilteredUser { + pub fn from(user: &User) -> Self { + FilteredUser { + id: user.id.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, +} + +#[derive(serde::Serialize, serde::Deserialize)] +pub struct TokenClaims { + pub sub: String, + pub exp: usize, + pub iat: usize, +} diff --git a/src/db/schema.rs b/src/db/schema.rs index a1a5577..fb1bf17 100644 --- a/src/db/schema.rs +++ b/src/db/schema.rs @@ -2,7 +2,7 @@ diesel::table! { users (id) { - id -> Int4, + id -> Uuid, login -> Text, hashed_password -> Text, name -> Text, diff --git a/src/error_handle.rs b/src/error_handle.rs new file mode 100644 index 0000000..eee86f9 --- /dev/null +++ b/src/error_handle.rs @@ -0,0 +1,25 @@ +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 61c426e..441c891 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,19 +1,26 @@ -mod config; -mod db; +pub mod api; +pub mod config; +pub mod db; +pub mod error_handle; +pub mod state; -use axum::{extract::State, http::StatusCode, response::Json, routing::get, Router}; +use axum::{ + extract::State, + http::StatusCode, + middleware, + response::Json, + routing::{get, post}, + Router, +}; use diesel::RunQueryDsl; use std::net::SocketAddr; +use std::sync::Arc; use std::{env, net::Ipv4Addr}; -use tracing_subscriber::{layer::SubscriberExt, util::SubscriberInitExt}; use crate::config::Config; -use db::{create_user, models::User}; - -pub struct AppState { - database: db::Pool, - config: Config, -} +use crate::db::{create_user, models::User}; +use crate::error_handle::*; +use crate::state::AppState; #[tokio::main] async fn main() { @@ -33,10 +40,10 @@ async fn main() { db::run_migrations(&pool).await; - let state = AppState { + let state = Arc::new(AppState { database: pool.clone(), config: config.clone(), - }; + }); let address: SocketAddr = format!("{}:{}", config.server.address, config.server.port) .parse() @@ -45,8 +52,19 @@ async fn main() { let lister = tokio::net::TcpListener::bind(&address).await.unwrap(); let app = Router::new() + .route("/api/v1/healthcheck", get(api::v1::healthcheck)) .route("/api/v1/users", get(users)) - .with_state(pool); + .route("/api/v1/register_user", post(api::v1::register_user)) + .route("/api/v1/login_user", post(api::v1::login_user)) + .route("/api/v1/logout_user", get(api::v1::logout_user)) + .route( + "/api/v1/me", + get(api::v1::me).route_layer(middleware::from_fn_with_state( + state.clone(), + api::v1::jwt_auth, + )), + ) + .with_state(state); println!("listening on http://{}", address); @@ -56,10 +74,12 @@ async fn main() { .unwrap(); } -async fn users(State(pool): State) -> Result>, (StatusCode, String)> { +async fn users( + State(state): State>, +) -> Result>, (StatusCode, Json)> { use db::schema::users::dsl::*; - let conn = pool.get().await.unwrap(); + let conn = state.database.get().await.unwrap(); let result = conn .interact(move |conn| users.load(conn)) @@ -70,22 +90,6 @@ async fn users(State(pool): State) -> Result>, (StatusC Ok(Json(result)) } -fn init_tracing() { - tracing_subscriber::registry() - .with( - tracing_subscriber::EnvFilter::try_from_default_env() - .unwrap_or_else(|_| "example_tokio_postgres=debug".into()), - ) - .with(tracing_subscriber::fmt::layer()) - .init(); -} - -fn internal_error(err: E) -> (StatusCode, String) -where - E: std::error::Error, -{ - (StatusCode::INTERNAL_SERVER_ERROR, err.to_string()) -} /* create_user( connection, diff --git a/src/state.rs b/src/state.rs new file mode 100644 index 0000000..6019116 --- /dev/null +++ b/src/state.rs @@ -0,0 +1,6 @@ +use crate::config::Config; + +pub struct AppState { + pub database: crate::db::Pool, + pub config: Config, +}