//! Various user types. Some types marked server-only to help prevent //! leaking passwords to the frontend use serde::{Deserialize, Serialize}; /// Standard informational user type, contains no password information #[derive(Clone, Debug, Deserialize, Serialize)] #[cfg_attr(feature = "server", derive(Queryable, Selectable, Identifiable))] #[cfg_attr(feature = "server", diesel(table_name = crate::schema::users, check_for_backend(diesel::pg::Pg)))] pub struct User { pub id: i32, pub username: String, pub created_at: chrono::DateTime, } /// Plaintext user credentials, used for login/signup form #[derive(Deserialize, Serialize)] pub struct UserCredentials { pub username: String, pub password: String, } cfg_if::cfg_if! { if #[cfg(feature = "server")] { use diesel::{ deserialize::{FromSql, FromSqlRow}, expression::AsExpression, prelude::*, serialize::ToSql, sql_types, }; use pbkdf2::{ PasswordHasher, PasswordVerifier, Pbkdf2, password_hash::Error::PasswordInvalid, phc::PasswordHash, }; use crate::util::error::{Error, Result}; /// Newtype for a `String`-represented hashed password #[derive(Clone, Debug, AsExpression, FromSqlRow)] #[diesel(sql_type = sql_types::Text)] pub struct HashedPassword(String); impl HashedPassword { /// Check a password attempt against this hashed password /// /// # Returns /// /// `Ok(true)` for a correct password /// `Ok(false)` for an incorrect password /// `Err` for a hashing error pub fn check(&self, password_attempt: String) -> Result { let pw_hash = PasswordHash::new(&self.0) .map_err(|e| Error::message_here(format!("Error parsing `HashedPassword`: {e}")))?; match Pbkdf2::default().verify_password(password_attempt.as_bytes(), &pw_hash) { Ok(()) => Ok(true), Err(PasswordInvalid) => Ok(false), Err(e) => Err(Error::message_here(format!( "Error comparing password attempt against hash: {e}" ))), } } /// Returns the "session auth hash" for `axum-login`, just the hashed password as bytes pub fn auth_hash(&self) -> &[u8] { self.0.as_bytes() } } impl FromSql for HashedPassword where DB: diesel::backend::Backend, String: FromSql, { fn from_sql(bytes: DB::RawValue<'_>) -> diesel::deserialize::Result { Ok(Self(String::from_sql(bytes)?)) } } impl ToSql for HashedPassword where DB: diesel::backend::Backend, String: ToSql, { fn to_sql<'b>( &'b self, out: &mut diesel::serialize::Output<'b, '_, DB>, ) -> diesel::serialize::Result { self.0.to_sql(out) } } /// User as it appears in the database, with hashed password #[derive(Clone, Debug, Identifiable, Queryable, Selectable)] #[diesel(table_name = crate::schema::users, check_for_backend(diesel::pg::Pg))] pub struct DbUser { pub id: i32, pub username: String, pub hashed_password: HashedPassword, pub created_at: chrono::DateTime, } impl From for User { fn from(db_user: DbUser) -> Self { User { id: db_user.id, username: db_user.username, created_at: db_user.created_at, } } } /// User credentials with hashed password #[derive(Clone, Debug, Insertable, Queryable, Selectable)] #[diesel(table_name = crate::schema::users, check_for_backend(diesel::pg::Pg))] pub struct HashedUserCredentials { username: String, hashed_password: HashedPassword, } impl From for HashedUserCredentials { fn from(db_user: DbUser) -> Self { HashedUserCredentials { username: db_user.username, hashed_password: db_user.hashed_password, } } } impl UserCredentials { /// Attempt to convert into `HashedUserCredentials` by hashing the password. Yields a PBKDF2 /// error on failure. pub fn try_hash(self) -> Result { let hashed_password = Pbkdf2::default().hash_password(self.password.as_bytes())?; Ok(HashedUserCredentials { username: self.username, hashed_password: HashedPassword(hashed_password.to_string()), }) } } } }