From 97cf3f62adf9fe3ffd19c6ff8cec5bc55064ba37 Mon Sep 17 00:00:00 2001 From: Ethan Girouard Date: Sat, 27 Jun 2026 13:24:12 -0400 Subject: [PATCH] Add User models --- src/models/mod.rs | 2 +- src/models/user.rs | 112 +++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 113 insertions(+), 1 deletion(-) create mode 100644 src/models/user.rs diff --git a/src/models/mod.rs b/src/models/mod.rs index 8b13789..22d12a3 100644 --- a/src/models/mod.rs +++ b/src/models/mod.rs @@ -1 +1 @@ - +pub mod user; diff --git a/src/models/user.rs b/src/models/user.rs new file mode 100644 index 0000000..6769c95 --- /dev/null +++ b/src/models/user.rs @@ -0,0 +1,112 @@ +//! Various user types. Some types marked server-only to help prevent +//! leaking passwords to the frontend + +/// Standard informational user type, contains no password information +#[derive(Clone, Debug)] +#[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 +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, Pbkdf2}; + +/// Newtype for a `String`-represented hashed password +#[derive(Clone, Debug, AsExpression, FromSqlRow)] +#[diesel(sql_type = sql_types::Text)] +pub struct HashedPassword(String); + +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()), + }) + } +} + +} +}