From 3ef53b7d480f3cfaa4b55e835d3cc9db116fe0b2 Mon Sep 17 00:00:00 2001 From: Ethan Girouard Date: Mon, 29 Jan 2024 19:52:02 -0500 Subject: [PATCH 1/9] Add actix_identity package --- Cargo.lock | 190 ++++++++++++++++++++++++++++++++++++++++++++++++++++- Cargo.toml | 2 + 2 files changed, 189 insertions(+), 3 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 5f2a07e..d297a86 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -53,7 +53,7 @@ dependencies = [ "actix-service", "actix-utils", "ahash 0.8.6", - "base64", + "base64 0.21.5", "bitflags 2.4.1", "brotli", "bytes", @@ -81,6 +81,22 @@ dependencies = [ "zstd", ] +[[package]] +name = "actix-identity" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1478456bca71c7b04411da1afb0c506e16dec6863815207693b791247511027f" +dependencies = [ + "actix-service", + "actix-session", + "actix-utils", + "actix-web", + "derive_more", + "futures-core", + "serde", + "tracing", +] + [[package]] name = "actix-macros" version = "0.2.4" @@ -142,6 +158,22 @@ dependencies = [ "pin-project-lite", ] +[[package]] +name = "actix-session" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b671404ec72194d8af58c2bdaf51e3c477a0595056bd5010148405870dda8df2" +dependencies = [ + "actix-service", + "actix-utils", + "actix-web", + "anyhow", + "derive_more", + "serde", + "serde_json", + "tracing", +] + [[package]] name = "actix-utils" version = "3.0.1" @@ -219,6 +251,41 @@ version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe" +[[package]] +name = "aead" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d122413f284cf2d62fb1b7db97e02edb8cda96d769b16e443a4f6195e35662b0" +dependencies = [ + "crypto-common", + "generic-array", +] + +[[package]] +name = "aes" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac1f845298e95f983ff1944b728ae08b8cebab80d684f0a832ed0fc74dfa27e2" +dependencies = [ + "cfg-if", + "cipher", + "cpufeatures", +] + +[[package]] +name = "aes-gcm" +version = "0.10.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "831010a0f742e1209b3bcea8fab6a8e149051ba6099432c8cb2cc117dec3ead1" +dependencies = [ + "aead", + "aes", + "cipher", + "ctr", + "ghash", + "subtle", +] + [[package]] name = "ahash" version = "0.7.7" @@ -356,6 +423,12 @@ dependencies = [ "rustc-demangle", ] +[[package]] +name = "base64" +version = "0.20.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ea22880d78093b0cbe17c89f64a7d457941e65759157ec6cb31a31d652b05e5" + [[package]] name = "base64" version = "0.21.5" @@ -547,6 +620,16 @@ dependencies = [ "half", ] +[[package]] +name = "cipher" +version = "0.4.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "773f3b9af64447d2ce9850330c473515014aa235e6a783b02db81ff39e4a3dad" +dependencies = [ + "crypto-common", + "inout", +] + [[package]] name = "collection_literals" version = "1.0.1" @@ -618,7 +701,14 @@ version = "0.16.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e859cd57d0710d9e06c381b550c06e76992472a8c6d527aecd2fc673dcc231fb" dependencies = [ + "aes-gcm", + "base64 0.20.0", + "hkdf", + "hmac", "percent-encoding", + "rand", + "sha2", + "subtle", "time", "version_check", ] @@ -664,9 +754,19 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3" dependencies = [ "generic-array", + "rand_core", "typenum", ] +[[package]] +name = "ctr" +version = "0.9.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0369ee1ad671834580515889b80f2ea915f23b8be8d0daa4bbaf2ac5c7590835" +dependencies = [ + "cipher", +] + [[package]] name = "darling" version = "0.14.4" @@ -778,6 +878,7 @@ checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" dependencies = [ "block-buffer", "crypto-common", + "subtle", ] [[package]] @@ -971,6 +1072,16 @@ dependencies = [ "wasm-bindgen", ] +[[package]] +name = "ghash" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d930750de5717d2dd0b8c0d42c076c0e884c81a73e6cab859bbd2339c71e3e40" +dependencies = [ + "opaque-debug", + "polyval", +] + [[package]] name = "gimli" version = "0.28.1" @@ -1060,6 +1171,24 @@ dependencies = [ "allocator-api2", ] +[[package]] +name = "hkdf" +version = "0.12.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7b5f8eb2ad728638ea2c7d47a21db23b7b58a72ed6a38256b8a1849f15fbbdf7" +dependencies = [ + "hmac", +] + +[[package]] +name = "hmac" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c49c37c09c17a53d937dfbb742eb3a961d65a994e6bcdcf37e7399d0cc8ab5e" +dependencies = [ + "digest", +] + [[package]] name = "html-escape" version = "0.2.13" @@ -1184,6 +1313,15 @@ dependencies = [ "hashbrown 0.14.3", ] +[[package]] +name = "inout" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a0c10553d664a4d0bcff9f4215d0aac67a639cc68ef660840afe309b807bc9f5" +dependencies = [ + "generic-array", +] + [[package]] name = "instant" version = "0.1.12" @@ -1434,7 +1572,7 @@ version = "0.5.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "22207568e096ac153ba8da68635e3136c1ec614ea9012736fa861c05bfb2eeff" dependencies = [ - "base64", + "base64 0.21.5", "cfg-if", "futures", "indexmap", @@ -1514,6 +1652,7 @@ name = "libretunes" version = "0.1.0" dependencies = [ "actix-files", + "actix-identity", "actix-web", "cfg-if", "console_error_panic_hook", @@ -1680,6 +1819,12 @@ version = "1.19.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3fdb12b2476b595f9358c5161aa467c2438859caa136dec86c26fdd2efe17b92" +[[package]] +name = "opaque-debug" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "624a8340c38c1b80fd549087862da4ba43e08858af025b236e509b6649fc13d5" + [[package]] name = "openssl" version = "0.10.63" @@ -1803,6 +1948,18 @@ version = "0.3.28" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "69d3587f8a9e599cc7ec2c00e331f71c4e69a5f9a4b8a6efd5b07466b9736f9a" +[[package]] +name = "polyval" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d52cff9d1d4dee5fe6d03729099f4a310a41179e0a10dbf542039873f2e826fb" +dependencies = [ + "cfg-if", + "cpufeatures", + "opaque-debug", + "universal-hash", +] + [[package]] name = "powerfmt" version = "0.2.0" @@ -2042,7 +2199,7 @@ version = "0.11.23" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "37b1ae8d9ac08420c66222fb9096fc5de435c3c48542bc5336c51892cffafb41" dependencies = [ - "base64", + "base64 0.21.5", "bytes", "encoding_rs", "futures-core", @@ -2318,6 +2475,17 @@ dependencies = [ "digest", ] +[[package]] +name = "sha2" +version = "0.10.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "793db75ad2bcafc3ffa7c68b215fee268f537982cd901d132f89c6343f3a3dc8" +dependencies = [ + "cfg-if", + "cpufeatures", + "digest", +] + [[package]] name = "signal-hook-registry" version = "1.4.1" @@ -2374,6 +2542,12 @@ version = "0.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623" +[[package]] +name = "subtle" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "81cdd64d312baedb58e21336b31bc043b77e01cc99033ce76ef539f78e965ebc" + [[package]] name = "syn" version = "1.0.109" @@ -2651,6 +2825,16 @@ version = "0.2.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f962df74c8c05a667b5ee8bcf162993134c104e96440b663c8daa176dc772d8c" +[[package]] +name = "universal-hash" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc1de2c688dc15305988b563c3854064043356019f97a4b46276fe734c4f07ea" +dependencies = [ + "crypto-common", + "subtle", +] + [[package]] name = "url" version = "2.5.0" diff --git a/Cargo.toml b/Cargo.toml index 5952557..fb58d19 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -29,6 +29,7 @@ diesel = { version = "2.1.4", features = ["postgres", "r2d2"], optional = true } lazy_static = { version = "1.4.0", optional = true } serde = { versions = "1.0.195", features = ["derive"] } openssl = { version = "0.10.63", optional = true } +actix-identity = { version = "0.7.0", optional = true } [features] csr = ["leptos/csr", "leptos_meta/csr", "leptos_router/csr"] @@ -44,6 +45,7 @@ ssr = [ "diesel", "lazy_static", "openssl", + "actix-identity", ] # Defines a size-optimized profile for the WASM bundle in release mode From 67f2a470f7e2ee4fc493508106ce6386d83cc912 Mon Sep 17 00:00:00 2001 From: Ethan Girouard Date: Sun, 4 Feb 2024 17:26:42 -0500 Subject: [PATCH 2/9] Add actix_session package --- Cargo.lock | 56 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ Cargo.toml | 2 ++ 2 files changed, 58 insertions(+) diff --git a/Cargo.lock b/Cargo.lock index d297a86..4b63124 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -169,6 +169,8 @@ dependencies = [ "actix-web", "anyhow", "derive_more", + "rand", + "redis", "serde", "serde_json", "tracing", @@ -346,6 +348,12 @@ version = "1.0.77" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c9d19de80eff169429ac1e9f48fffb163916b448a44e8e046186232046d9e1f9" +[[package]] +name = "arc-swap" +version = "1.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bddcadddf5e9015d310179a59bb28c4d4b9920ad0f11e8e14dbadf654890c9a6" + [[package]] name = "askama_escape" version = "0.10.3" @@ -636,6 +644,20 @@ version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "186dce98367766de751c42c4f03970fc60fc012296e706ccbb9d5df9b6c1e271" +[[package]] +name = "combine" +version = "4.6.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "35ed6e9d84f0b51a7f52daf1c7d71dd136fd7a3f41a8462b8cdb8c78d920fad4" +dependencies = [ + "bytes", + "futures-core", + "memchr", + "pin-project-lite", + "tokio", + "tokio-util", +] + [[package]] name = "config" version = "0.13.4" @@ -1653,6 +1675,7 @@ version = "0.1.0" dependencies = [ "actix-files", "actix-identity", + "actix-session", "actix-web", "cfg-if", "console_error_panic_hook", @@ -2146,6 +2169,28 @@ dependencies = [ "getrandom", ] +[[package]] +name = "redis" +version = "0.24.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c580d9cbbe1d1b479e8d67cf9daf6a62c957e6846048408b80b43ac3f6af84cd" +dependencies = [ + "arc-swap", + "async-trait", + "bytes", + "combine", + "futures", + "futures-util", + "itoa", + "percent-encoding", + "pin-project-lite", + "ryu", + "tokio", + "tokio-retry", + "tokio-util", + "url", +] + [[package]] name = "redox_syscall" version = "0.4.1" @@ -2690,6 +2735,17 @@ dependencies = [ "windows-sys", ] +[[package]] +name = "tokio-retry" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f57eb36ecbe0fc510036adff84824dd3c24bb781e21bfa67b69d556aa85214f" +dependencies = [ + "pin-project", + "rand", + "tokio", +] + [[package]] name = "tokio-util" version = "0.7.10" diff --git a/Cargo.toml b/Cargo.toml index fb58d19..259386f 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -30,6 +30,7 @@ lazy_static = { version = "1.4.0", optional = true } serde = { versions = "1.0.195", features = ["derive"] } openssl = { version = "0.10.63", optional = true } actix-identity = { version = "0.7.0", optional = true } +actix-session = { version = "0.9.0", features = ["redis-rs-session"], optional = true } [features] csr = ["leptos/csr", "leptos_meta/csr", "leptos_router/csr"] @@ -46,6 +47,7 @@ ssr = [ "lazy_static", "openssl", "actix-identity", + "actix-session", ] # Defines a size-optimized profile for the WASM bundle in release mode From 35eee117d796bc60bb9512a6ddaa2567272d38c3 Mon Sep 17 00:00:00 2001 From: Ethan Girouard Date: Sun, 4 Feb 2024 17:34:45 -0500 Subject: [PATCH 3/9] Add PublicUser model --- src/models.rs | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/src/models.rs b/src/models.rs index 2daf3ae..d93bc97 100644 --- a/src/models.rs +++ b/src/models.rs @@ -43,3 +43,20 @@ pub struct NewUser { /// The user's password, stored as a hash pub password: String, } + +/// Model for a "Public User", used for returning user data to the client +/// This model omits the password field, so that the hashed password is not sent to the client +#[cfg_attr(feature = "ssr", derive(Queryable, Selectable))] +#[cfg_attr(feature = "ssr", diesel(table_name = crate::schema::users))] +#[cfg_attr(feature = "ssr", diesel(check_for_backend(diesel::pg::Pg)))] +#[derive(Serialize, Deserialize)] +pub struct PublicUser { + /// A unique id for the user + pub id: i32, + /// The user's username + pub username: String, + /// The user's email + pub email: String, + /// The time the user was created + pub created_at: SystemTime, +} From ee5e8694425423b41c5afc975e27c867ac42fbce Mon Sep 17 00:00:00 2001 From: Ethan Girouard Date: Sun, 4 Feb 2024 17:43:17 -0500 Subject: [PATCH 4/9] Implement User conversions --- src/models.rs | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/src/models.rs b/src/models.rs index d93bc97..fec0ab6 100644 --- a/src/models.rs +++ b/src/models.rs @@ -44,6 +44,17 @@ pub struct NewUser { pub password: String, } +/// Convert a User into a NewUser, omitting the id and created_at fields +impl From for NewUser { + fn from(user: User) -> NewUser { + NewUser { + username: user.username, + email: user.email, + password: user.password, + } + } +} + /// Model for a "Public User", used for returning user data to the client /// This model omits the password field, so that the hashed password is not sent to the client #[cfg_attr(feature = "ssr", derive(Queryable, Selectable))] @@ -60,3 +71,15 @@ pub struct PublicUser { /// The time the user was created pub created_at: SystemTime, } + +/// Convert a User into a PublicUser, omitting the password field +impl From for PublicUser { + fn from(user: User) -> PublicUser { + PublicUser { + id: user.id, + username: user.username, + email: user.email, + created_at: user.created_at, + } + } +} From 6d35aa4d786eba8e14d388365b14bc3cb408a767 Mon Sep 17 00:00:00 2001 From: Ethan Girouard Date: Sun, 4 Feb 2024 17:46:48 -0500 Subject: [PATCH 5/9] Add pbkdf2 package --- Cargo.lock | 30 ++++++++++++++++++++++++++++++ Cargo.toml | 2 ++ 2 files changed, 32 insertions(+) diff --git a/Cargo.lock b/Cargo.lock index 4b63124..2da0866 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -443,6 +443,12 @@ version = "0.21.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "35636a1494ede3b646cc98f74f8e62c773a38a659ebc777a2cf26b9b74171df9" +[[package]] +name = "base64ct" +version = "1.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8c3c1a368f70d6cf7302d78f8f7093da241fb8e8807c05cc9e51a125895a6d5b" + [[package]] name = "bitflags" version = "1.3.2" @@ -1689,6 +1695,7 @@ dependencies = [ "leptos_meta", "leptos_router", "openssl", + "pbkdf2", "serde", "wasm-bindgen", ] @@ -1915,6 +1922,17 @@ dependencies = [ "windows-targets", ] +[[package]] +name = "password-hash" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "346f04948ba92c43e8469c1ee6736c7563d71012b17d40745260fe106aac2166" +dependencies = [ + "base64ct", + "rand_core", + "subtle", +] + [[package]] name = "paste" version = "1.0.14" @@ -1927,6 +1945,18 @@ version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8835116a5c179084a830efb3adc117ab007512b535bc1a21c991d3b32a6b44dd" +[[package]] +name = "pbkdf2" +version = "0.12.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8ed6a7761f76e3b9f92dfb0a60a6a6477c61024b775147ff0973a02653abaf2" +dependencies = [ + "digest", + "hmac", + "password-hash", + "sha2", +] + [[package]] name = "percent-encoding" version = "2.3.1" diff --git a/Cargo.toml b/Cargo.toml index 259386f..59e9c73 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -31,6 +31,7 @@ serde = { versions = "1.0.195", features = ["derive"] } openssl = { version = "0.10.63", optional = true } actix-identity = { version = "0.7.0", optional = true } actix-session = { version = "0.9.0", features = ["redis-rs-session"], optional = true } +pbkdf2 = { version = "0.12.2", features = ["simple"], optional = true } [features] csr = ["leptos/csr", "leptos_meta/csr", "leptos_router/csr"] @@ -48,6 +49,7 @@ ssr = [ "openssl", "actix-identity", "actix-session", + "pbkdf2", ] # Defines a size-optimized profile for the WASM bundle in release mode From 7013b2e22e0a9c4721ab4460300e5ec1447507eb Mon Sep 17 00:00:00 2001 From: Ethan Girouard Date: Sun, 4 Feb 2024 21:22:05 -0500 Subject: [PATCH 6/9] Add basic database functions for users --- src/lib.rs | 1 + src/users.rs | 93 ++++++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 94 insertions(+) create mode 100644 src/users.rs diff --git a/src/lib.rs b/src/lib.rs index 28db59f..ad311de 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -4,6 +4,7 @@ pub mod playstatus; pub mod playbar; pub mod database; pub mod models; +pub mod users; use cfg_if::cfg_if; cfg_if! { diff --git a/src/users.rs b/src/users.rs new file mode 100644 index 0000000..d2a42c9 --- /dev/null +++ b/src/users.rs @@ -0,0 +1,93 @@ +cfg_if::cfg_if! { + if #[cfg(feature = "ssr")] { + use diesel::prelude::*; + use crate::database::get_db_conn; + + use pbkdf2::{ + password_hash::{ + rand_core::OsRng, + PasswordHasher, PasswordHash, SaltString, PasswordVerifier, Error + }, + Pbkdf2 + }; + } +} + +use leptos::*; +use crate::models::{NewUser, PublicUser, User}; + +/// Get a user from the database by username or email +/// Returns a Result with the user if found, None if not found, or an error if there was a problem +#[cfg(feature = "ssr")] +pub async fn find_user(username_or_email: String) -> Result, ServerFnError> { + use crate::schema::users::dsl::*; + + // Look for either a username or email that matches the input, and return an option with None if no user is found + let db_con = &mut get_db_conn(); + let user = users.filter(username.eq(username_or_email.clone())).or_filter(email.eq(username_or_email)) + .first::(db_con).optional() + .map_err(|e| ServerFnError::ServerError(format!("Error getting user from database: {}", e)))?; + + Ok(user) +} + +/// Create a new user in the database +/// Returns an empty Result if successful, or an error if there was a problem +#[cfg(feature = "ssr")] +pub async fn create_user(new_user: &NewUser) -> Result<(), ServerFnError> { + use crate::schema::users::dsl::*; + + let salt = SaltString::generate(&mut OsRng); + let password_hash = Pbkdf2.hash_password(new_user.password.as_bytes(), &salt) + .map_err(|_| ServerFnError::ServerError("Error hashing password".to_string()))?.to_string(); + + let new_user = NewUser { + username: new_user.username.clone(), + email: new_user.email.clone(), + password: password_hash, + }; + + let db_con = &mut get_db_conn(); + + diesel::insert_into(users).values(&new_user).execute(db_con) + .map_err(|e| ServerFnError::ServerError(format!("Error creating user: {}", e)))?; + + Ok(()) +} + +/// Validate a user's credentials +/// Returns a Result with the user if the credentials are valid, None if not valid, or an error if there was a problem +#[cfg(feature = "ssr")] +pub async fn validate_user(username_or_email: String, password: String) -> Result, ServerFnError> { + let db_user = find_user(username_or_email.clone()).await + .map_err(|e| ServerFnError::ServerError(format!("Error getting user from database: {}", e)))?; + + // If the user is not found, return None + let db_user = match db_user { + Some(user) => user, + None => return Ok(None) + }; + + let password_hash = PasswordHash::new(&db_user.password) + .map_err(|e| ServerFnError::ServerError(format!("Error hashing supplied password: {}", e)))?; + + match Pbkdf2.verify_password(password.as_bytes(), &password_hash) { + Ok(()) => {}, + Err(Error::Password) => { + return Ok(None); + }, + Err(e) => { + return Err(ServerFnError::ServerError(format!("Error verifying password: {}", e))); + } + } + + Ok(Some(db_user)) +} + +/// Get a user from the database by username or email +/// Returns a Result with the user if found, None if not found, or an error if there was a problem +#[server(endpoint = "get_user")] +pub async fn get_user(username_or_email: String) -> Result, ServerFnError> { + let user = find_user(username_or_email).await?; + Ok(user.map(|u| u.into())) +} From 8f9d7b5bc5f18d06439dc0c86b8dfc548a42ea32 Mon Sep 17 00:00:00 2001 From: Ethan Girouard Date: Sun, 4 Feb 2024 21:22:49 -0500 Subject: [PATCH 7/9] Implement authentication on backend --- src/auth.rs | 65 +++++++++++++++++++++++++++++++++++++++++++++++++++++ src/lib.rs | 1 + 2 files changed, 66 insertions(+) create mode 100644 src/auth.rs diff --git a/src/auth.rs b/src/auth.rs new file mode 100644 index 0000000..1d702a3 --- /dev/null +++ b/src/auth.rs @@ -0,0 +1,65 @@ +use leptos::*; +use crate::models::NewUser; + +/// Create a new user and log them in +/// Takes in a NewUser struct, with the password in plaintext +/// Returns a Result with the error message if the user could not be created +#[server(endpoint = "signup")] +pub async fn signup(new_user: NewUser) -> Result<(), ServerFnError> { + use crate::users::create_user; + + use leptos_actix::extract; + use actix_web::{HttpMessage, HttpRequest}; + use actix_identity::Identity; + + create_user(&new_user).await + .map_err(|e| ServerFnError::ServerError(format!("Error creating user: {}", e)))?; + + extract(|request: HttpRequest| async move { + Identity::login(&request.extensions(), new_user.username.clone()) + }).await??; + + Ok(()) +} + +/// Log a user in +/// Takes in a username or email and a password in plaintext +/// Returns a Result with a boolean indicating if the login was successful +#[server(endpoint = "login")] +pub async fn login(username_or_email: String, password: String) -> Result { + use crate::users::validate_user; + use actix_web::{HttpMessage, HttpRequest}; + use actix_identity::Identity; + use leptos_actix::extract; + + let possible_user = validate_user(username_or_email, password).await + .map_err(|e| ServerFnError::ServerError(format!("Error validating user: {}", e)))?; + + let user = match possible_user { + Some(user) => user, + None => return Ok(false) + }; + + extract(|request: HttpRequest| async move { + Identity::login(&request.extensions(), user.username.clone()) + }).await??; + + Ok(true) +} + +/// Log a user out +/// Returns a Result with the error message if the user could not be logged out +#[server(endpoint = "logout")] +pub async fn logout() -> Result<(), ServerFnError> { + use leptos_actix::extract; + use actix_identity::Identity; + + extract(|user: Option| async move { + if let Some(user) = user { + user.logout(); + } + }).await?; + + Ok(()) +} + diff --git a/src/lib.rs b/src/lib.rs index ad311de..5eaf272 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,4 +1,5 @@ pub mod app; +pub mod auth; pub mod songdata; pub mod playstatus; pub mod playbar; From 960d0d4662d022682d0192917ce7c7d723a0e7e1 Mon Sep 17 00:00:00 2001 From: Ethan Girouard Date: Sun, 4 Feb 2024 22:04:37 -0500 Subject: [PATCH 8/9] Add identity and session middlewares --- src/main.rs | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/src/main.rs b/src/main.rs index 91dde65..b16a7c2 100644 --- a/src/main.rs +++ b/src/main.rs @@ -11,9 +11,23 @@ extern crate diesel; #[cfg(feature = "ssr")] #[actix_web::main] async fn main() -> std::io::Result<()> { + use actix_identity::IdentityMiddleware; + use actix_session::storage::RedisSessionStore; + use actix_session::SessionMiddleware; + use actix_web::cookie::Key; + use dotenv::dotenv; dotenv().ok(); + let session_secret_key = if let Ok(key) = std::env::var("SESSION_SECRET_KEY") { + Key::from(key.as_bytes()) + } else { + Key::generate() + }; + + let redis_url = std::env::var("REDIS_URL").expect("REDIS_URL must be set"); + let redis_store = RedisSessionStore::new(redis_url).await.unwrap(); + use actix_files::Files; use actix_web::*; use leptos::*; @@ -40,6 +54,8 @@ async fn main() -> std::io::Result<()> { .service(favicon) .leptos_routes(leptos_options.to_owned(), routes.to_owned(), App) .app_data(web::Data::new(leptos_options.to_owned())) + .wrap(IdentityMiddleware::default()) + .wrap(SessionMiddleware::new(redis_store.clone(), session_secret_key.clone())) //.wrap(middleware::Compress::default()) }) .bind(&addr)? From 256b999391640736dfde1c52196de9b87b8d1fd0 Mon Sep 17 00:00:00 2001 From: Ethan Girouard Date: Thu, 8 Feb 2024 18:34:51 -0500 Subject: [PATCH 9/9] Merge user models into a single struct --- src/auth.rs | 10 +++++-- src/models.rs | 75 +++++++++------------------------------------------ src/users.rs | 33 +++++++++++++++-------- 3 files changed, 43 insertions(+), 75 deletions(-) diff --git a/src/auth.rs b/src/auth.rs index 1d702a3..1995deb 100644 --- a/src/auth.rs +++ b/src/auth.rs @@ -1,17 +1,23 @@ use leptos::*; -use crate::models::NewUser; +use crate::models::User; /// Create a new user and log them in /// Takes in a NewUser struct, with the password in plaintext /// Returns a Result with the error message if the user could not be created #[server(endpoint = "signup")] -pub async fn signup(new_user: NewUser) -> Result<(), ServerFnError> { +pub async fn signup(new_user: User) -> Result<(), ServerFnError> { use crate::users::create_user; use leptos_actix::extract; use actix_web::{HttpMessage, HttpRequest}; use actix_identity::Identity; + // Ensure the user has no id + let new_user = User { + id: None, + ..new_user + }; + create_user(&new_user).await .map_err(|e| ServerFnError::ServerError(format!("Error creating user: {}", e)))?; diff --git a/src/models.rs b/src/models.rs index fec0ab6..e6ee017 100644 --- a/src/models.rs +++ b/src/models.rs @@ -11,75 +11,26 @@ use diesel::prelude::*; // diesel-specific attributes to the models when compiling for the server /// Model for a "User", used for querying the database -#[cfg_attr(feature = "ssr", derive(Queryable, Selectable))] -#[cfg_attr(feature = "ssr", diesel(table_name = crate::schema::users))] -#[cfg_attr(feature = "ssr", diesel(check_for_backend(diesel::pg::Pg)))] -#[derive(Serialize, Deserialize)] -pub struct User { - /// A unique id for the user - pub id: i32, - /// The user's username - pub username: String, - /// The user's email - pub email: String, - /// The user's password, stored as a hash - pub password: String, - /// The time the user was created - pub created_at: SystemTime, -} - -/// Model for a "New User", used for inserting into the database -/// Note that this model does not have an id or created_at field, as those are automatically -/// generated by the database and we don't want to deal with them ourselves -#[cfg_attr(feature = "ssr", derive(Insertable))] +/// Various fields are wrapped in Options, because they are not always wanted for inserts/retrieval +/// Using deserialize_as makes Diesel use the specified type when deserializing from the database, +/// and then call .into() to convert it into the Option +#[cfg_attr(feature = "ssr", derive(Queryable, Selectable, Insertable))] #[cfg_attr(feature = "ssr", diesel(table_name = crate::schema::users))] #[cfg_attr(feature = "ssr", diesel(check_for_backend(diesel::pg::Pg)))] #[derive(Serialize, Deserialize, Clone, Debug)] -pub struct NewUser { +pub struct User { + /// A unique id for the user + #[cfg_attr(feature = "ssr", diesel(deserialize_as = i32))] + // #[cfg_attr(feature = "ssr", diesel(skip_insertion))] // This feature is not yet released + pub id: Option, /// The user's username pub username: String, /// The user's email pub email: String, /// The user's password, stored as a hash - pub password: String, -} - -/// Convert a User into a NewUser, omitting the id and created_at fields -impl From for NewUser { - fn from(user: User) -> NewUser { - NewUser { - username: user.username, - email: user.email, - password: user.password, - } - } -} - -/// Model for a "Public User", used for returning user data to the client -/// This model omits the password field, so that the hashed password is not sent to the client -#[cfg_attr(feature = "ssr", derive(Queryable, Selectable))] -#[cfg_attr(feature = "ssr", diesel(table_name = crate::schema::users))] -#[cfg_attr(feature = "ssr", diesel(check_for_backend(diesel::pg::Pg)))] -#[derive(Serialize, Deserialize)] -pub struct PublicUser { - /// A unique id for the user - pub id: i32, - /// The user's username - pub username: String, - /// The user's email - pub email: String, + #[cfg_attr(feature = "ssr", diesel(deserialize_as = String))] + pub password: Option, /// The time the user was created - pub created_at: SystemTime, -} - -/// Convert a User into a PublicUser, omitting the password field -impl From for PublicUser { - fn from(user: User) -> PublicUser { - PublicUser { - id: user.id, - username: user.username, - email: user.email, - created_at: user.created_at, - } - } + #[cfg_attr(feature = "ssr", diesel(deserialize_as = SystemTime))] + pub created_at: Option, } diff --git a/src/users.rs b/src/users.rs index d2a42c9..7d81b31 100644 --- a/src/users.rs +++ b/src/users.rs @@ -14,7 +14,7 @@ cfg_if::cfg_if! { } use leptos::*; -use crate::models::{NewUser, PublicUser, User}; +use crate::models::User; /// Get a user from the database by username or email /// Returns a Result with the user if found, None if not found, or an error if there was a problem @@ -34,17 +34,19 @@ pub async fn find_user(username_or_email: String) -> Result, Server /// Create a new user in the database /// Returns an empty Result if successful, or an error if there was a problem #[cfg(feature = "ssr")] -pub async fn create_user(new_user: &NewUser) -> Result<(), ServerFnError> { +pub async fn create_user(new_user: &User) -> Result<(), ServerFnError> { use crate::schema::users::dsl::*; + let new_password = new_user.password.clone() + .ok_or(ServerFnError::ServerError(format!("No password provided for user {}", new_user.username)))?; + let salt = SaltString::generate(&mut OsRng); - let password_hash = Pbkdf2.hash_password(new_user.password.as_bytes(), &salt) + let password_hash = Pbkdf2.hash_password(new_password.as_bytes(), &salt) .map_err(|_| ServerFnError::ServerError("Error hashing password".to_string()))?.to_string(); - let new_user = NewUser { - username: new_user.username.clone(), - email: new_user.email.clone(), - password: password_hash, + let new_user = User { + password: Some(password_hash), + ..new_user.clone() }; let db_con = &mut get_db_conn(); @@ -68,7 +70,10 @@ pub async fn validate_user(username_or_email: String, password: String) -> Resul None => return Ok(None) }; - let password_hash = PasswordHash::new(&db_user.password) + let db_password = db_user.password.clone() + .ok_or(ServerFnError::ServerError(format!("No password found for user {}", db_user.username)))?; + + let password_hash = PasswordHash::new(&db_password) .map_err(|e| ServerFnError::ServerError(format!("Error hashing supplied password: {}", e)))?; match Pbkdf2.verify_password(password.as_bytes(), &password_hash) { @@ -87,7 +92,13 @@ pub async fn validate_user(username_or_email: String, password: String) -> Resul /// Get a user from the database by username or email /// Returns a Result with the user if found, None if not found, or an error if there was a problem #[server(endpoint = "get_user")] -pub async fn get_user(username_or_email: String) -> Result, ServerFnError> { - let user = find_user(username_or_email).await?; - Ok(user.map(|u| u.into())) +pub async fn get_user(username_or_email: String) -> Result, ServerFnError> { + let mut user = find_user(username_or_email).await?; + + // Remove the password hash before returning the user + if let Some(user) = user.as_mut() { + user.password = None; + } + + Ok(user) }