From 256b999391640736dfde1c52196de9b87b8d1fd0 Mon Sep 17 00:00:00 2001 From: Ethan Girouard Date: Thu, 8 Feb 2024 18:34:51 -0500 Subject: [PATCH] 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) }