use std::fmt::Display; use anyhow::{anyhow, Context}; use rand::{RngCore, SeedableRng}; use rand_chacha::ChaCha20Rng; use sqlx::{prelude::FromRow, SqlitePool}; use crate::{assets::Asset, hash_value, verify_hashed_value, BlogDb}; #[derive(Debug, PartialEq)] pub struct UserInfo { pub name: String, pub about: String, pub home: String, } impl From for UserInfo { fn from(value: User) -> Self { Self { name: value.name, about: value.about, home: value.home, } } } #[derive(Clone, FromRow, Default)] struct User { name: String, password_hash: String, about: String, // this isn't great but these r the home and about page html home: String, } impl Display for User { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { write!( f, "User(name: {}, password_hash: ###, homepage: {:?})", self.name, self.home ) } } impl BlogDb { pub(super) async fn verify_password(&self, user: S, password: String) -> anyhow::Result<()> where S: AsRef, { let user: User = sqlx::query_as("SELECT * FROM users WHERE name=?") .bind(user.as_ref()) .fetch_one(&self.db) .await?; let hash = user.password_hash; verify_hashed_value(password, hash).await } pub(super) async fn add_initial_user( db: &SqlitePool, username: S, password: String, ) -> anyhow::Result<()> where S: AsRef, { let password_hash = hash_value(password).await?; sqlx::query("INSERT OR IGNORE INTO users (name, password_hash) VALUES (?,?)") .bind(username.as_ref()) .bind(password_hash) .execute(db) .await .with_context(|| "Something went wrong while executing sqlite query")?; Ok(()) } /// Update password with new string, authenticated with current valid password hash pub async fn update_password( &self, user: S, password: String, session_id: i64, ) -> anyhow::Result<()> where S: AsRef, { if user.as_ref() != &self.check_session(session_id).await? { return Err(anyhow::Error::msg( "Updating password, username doesn't match session username", )); }; let hash = hash_value(password.clone()).await?; sqlx::query("UPDATE users SET (password_hash) = ?") .bind(&hash) .execute(&self.db) .await?; sqlx::query("UPDATE sessions SET (password_hash) = ? WHERE user = ?") .bind(&hash) .bind(user.as_ref()) .execute(&self.db) .await?; Ok(()) } pub async fn get_user(&self) -> UserInfo { let user: UserInfo = sqlx::query_as::<_, User>("SELECT * FROM users") .fetch_one(&self.db) .await .unwrap_or_default() .into(); user } pub async fn get_user_info(&self, user: S) -> anyhow::Result where S: AsRef, { let user: User = sqlx::query_as("SELECT * FROM users WHERE name=?") .bind(user.as_ref()) .fetch_one(&self.db) .await?; Ok(user.into()) } pub async fn update_user_name( &self, user: S, name: T, session_id: i64, ) -> anyhow::Result where S: AsRef, T: AsRef, { self.authorize_user(user.as_ref(), session_id) .await .map_err(|_| { anyhow!( "An unauthorized user tried to update user {}'s name", user.as_ref() ) })?; let info: UserInfo = sqlx::query_as::<_, User>("UPDATE users SET (name) = ? WHERE name=? RETURNING *") .bind(name.as_ref()) .bind(user.as_ref()) .fetch_one(&self.db) .await? .into(); sqlx::query("UPDATE sessions SET (user) = ? WHERE user=?") .bind(&info.name) .bind(user.as_ref()) .execute(&self.memory) .await?; Ok(info) } pub async fn get_homepage(&self) -> String { let user: User = sqlx::query_as("SELECT * FROM users") .fetch_one(&self.db) .await .unwrap_or_default(); user.home } pub async fn get_about(&self) -> String { let user: User = sqlx::query_as("SELECT * FROM users") .fetch_one(&self.db) .await .unwrap_or_default(); user.about } pub async fn update_user_homepage( &self, user: S, home: T, session_id: i64, ) -> anyhow::Result<()> where S: AsRef, T: AsRef, { self.authorize_user(user.as_ref(), session_id) .await .map_err(|_| { anyhow!( "An unauthorized user tried to update user {}'s name", user.as_ref() ) })?; sqlx::query("UPDATE users SET (home) = ? WHERE name=? RETURNING *") .bind(home.as_ref()) .bind(user.as_ref()) .execute(&self.db) .await?; Ok(()) } pub async fn update_user_about( &self, user: S, about: T, session_id: i64, ) -> anyhow::Result where S: AsRef, T: AsRef, { self.authorize_user(user.as_ref(), session_id) .await .map_err(|_| { anyhow!( "An unauthorized user tried to update user {}'s bio", user.as_ref() ) })?; let info = sqlx::query_as::<_, User>("UPDATE users SET (about) = ? WHERE name=? RETURNING *") .bind(about.as_ref()) .bind(user.as_ref()) .fetch_one(&self.db) .await? .into(); Ok(info) } // /// Updates user profile pic with asset from database // pub async fn update_user_profile_pic( // &self, // user: S, // profile_pic_name: T, // session_id: i64, // ) -> anyhow::Result // where // S: AsRef, // T: AsRef, // { // self.authorize_user(user.as_ref(), session_id) // .await // .map_err(|_| { // anyhow!( // "An unauthorized user tried to update user {}'s profile pic", // user.as_ref() // ) // })?; // let info = sqlx::query_as::<_, User>( // "UPDATE users SET (profile_pic) = ? WHERE name=? RETURNING *", // ) // .bind(profile_pic_name.as_ref()) // .bind(user.as_ref()) // .fetch_one(&self.db) // .await? // .into(); // Ok(info) // } // /// Replaces user profile pic with new photo, saving it to the database as an asset // pub async fn replace_user_profile_pic( // &self, // user: S, // profile_pic_data: D, // session_id: i64, // ) -> anyhow::Result<(Asset, UserInfo)> // where // S: AsRef, // D: Into>, // { // self.authorize_user(user.as_ref(), session_id) // .await // .map_err(|_| { // anyhow!( // "An unauthorized user tried to replace user {}'s profile pic", // user.as_ref() // ) // })?; // let data = profile_pic_data.into(); // let user_name = self.get_user_info(user.as_ref()).await?.name; // let ext = infer::get(&data) // .ok_or(anyhow!("Couldn't get the profile pic filetype"))? // .extension(); // let name = format!("{user_name}-profile.{ext}"); // let profile_pic = self.add_asset(name, data).await?; // let updated_info: UserInfo = sqlx::query_as::<_, User>( // "UPDATE users SET (profile_pic) = ? WHERE name=? RETURNING *", // ) // .bind(&profile_pic.slug) // .bind(user.as_ref()) // .fetch_one(&self.db) // .await? // .into(); // Ok((profile_pic, updated_info)) // } /// Check if a given session id is valid, and if it is, get the user it's valid for pub async fn check_session(&self, session_id: I) -> anyhow::Result where I: Into, { let _ = sqlx::query("DELETE * FROM SESSIONS WHERE date( &self, user: S, password: String, expires: T, ) -> anyhow::Result where S: AsRef, T: AsRef, { self.verify_password(user.as_ref(), password).await?; // ChaCha20Rng panics if it can't get secure entropy, which prolly won't happen but still // want to catch if it does let id = std::panic::catch_unwind(|| ChaCha20Rng::from_entropy().next_u64()) .map_err(|_| anyhow!("Couldn't get secure entropy to generate session id"))? as i64; sqlx::query("INSERT INTO sessions VALUES (?,?, DATE('now', ?))") .bind(id) .bind(user.as_ref()) .bind(expires.as_ref()) .execute(&self.memory) .await?; Ok(id) } // /// Get a session for the server, returning a session id. // pub async fn get_server_session(&self) -> anyhow::Result { // // ChaCha20Rng panics if it can't get secure entropy, which prolly won't happen but still // // want to catch if it does // let id = std::panic::catch_unwind(|| ChaCha20Rng::from_entropy().next_u64()) // .map_err(|_| anyhow!("Couldn't get secure entropy to generate session id"))? // as i64; // let user: String = random::().to_string(); // sqlx::query("INSERT INTO sessions VALUES (?,?)") // .bind(id) // .bind(user) // .execute(&self.memory) // .await?; // Ok(id) // } /// End the user session, call this when logging out the user pub async fn end_session(&self, id: I) -> anyhow::Result<()> where I: Into, { sqlx::query("DELETE FROM sessions WHERE id=?") .bind(id.into()) .execute(&self.memory) .await?; Ok(()) } /// Check if a session is valid for a given user pub(super) async fn authorize_user(&self, user: S, session_id: I) -> anyhow::Result<()> where S: AsRef, I: Into + Copy, { let session_user = self.check_session(session_id).await?; // this could be a match or if/else statement but i think it's a funny one-liner (user.as_ref() == session_user).then_some(()).ok_or(anyhow!( "Session {} is not valid for user {}", session_id.into(), user.as_ref() )) } } #[cfg(test)] mod test { use super::*; use crate::util::tests::*; #[sqlx::test] async fn user_info(pool: SqlitePool) -> anyhow::Result<()> { let (db, session_id) = get_init_db(pool).await?; // Getting info let expected_info: UserInfo = UserInfo { name: "evie".to_string(), about: "".to_string(), home: None, }; assert_eq!(db.get_user_info("evie").await?, expected_info); // Updating info let user = db.get_user_info("evie").await?; let new_name = "august"; let new_bio = "loves george"; db.update_user_name(&user.name, new_name, session_id) .await?; db.update_user_about(new_name, new_bio, session_id).await?; // PNG magic numbers let profile_pic_data = &[0x89, 0x50, 0x4e, 0x47]; let profile_pic = db.add_asset("profile.png", profile_pic_data).await?; let updated_info = db .update_user_profile_pic(new_name, &profile_pic.slug, session_id) .await?; assert_eq!(updated_info.profile_pic, Some(profile_pic.slug)); db.replace_user_profile_pic(new_name, profile_pic_data, session_id) .await?; Ok(()) } #[sqlx::test] async fn sessions(pool: SqlitePool) -> anyhow::Result<()> { let (db, _) = get_init_db(pool).await?; let username = "evie"; let password = "hunter2".to_string(); let id = db.new_session(username, password, "+5 minutes").await?; let session_result = db.check_session(id).await; assert!(session_result.is_ok()); let logout = db.end_session(id).await; assert!(logout.is_ok()); Ok(()) } #[sqlx::test] async fn update_password(pool: SqlitePool) -> anyhow::Result<()> { let (db, session_id) = get_init_db(pool).await?; let update = db .update_password( "evie", "hunter2".to_string(), "password".to_string(), session_id, ) .await; assert!(update.is_ok()); Ok(()) } }