From a0e93f028759b67a90679717d268cefc99fd7d19 Mon Sep 17 00:00:00 2001 From: realaravinth Date: Sat, 10 Sep 2022 20:08:59 +0530 Subject: [PATCH] feat: auth db ops --- .../20220910140647_librepages_users.sql | 7 + sqlx-data.json | 179 +++++++++- src/db.rs | 330 +++++++++++++++++- src/errors.rs | 15 + 4 files changed, 528 insertions(+), 3 deletions(-) create mode 100644 migrations/20220910140647_librepages_users.sql diff --git a/migrations/20220910140647_librepages_users.sql b/migrations/20220910140647_librepages_users.sql new file mode 100644 index 0000000..5316ef1 --- /dev/null +++ b/migrations/20220910140647_librepages_users.sql @@ -0,0 +1,7 @@ +CREATE TABLE IF NOT EXISTS librepages_users ( + name VARCHAR(100) NOT NULL UNIQUE, + email VARCHAR(100) UNIQUE NOT NULL, + email_verified BOOLEAN DEFAULT NULL, + password TEXT NOT NULL, + ID SERIAL PRIMARY KEY NOT NULL +); diff --git a/sqlx-data.json b/sqlx-data.json index 95c8c85..e0b11f3 100644 --- a/sqlx-data.json +++ b/sqlx-data.json @@ -1,3 +1,180 @@ { - "db": "PostgreSQL" + "db": "PostgreSQL", + "1be33ea4fe0e6079c88768ff912b824f4b0250193f2d086046c1fd0da125ae0c": { + "describe": { + "columns": [ + { + "name": "name", + "ordinal": 0, + "type_info": "Varchar" + }, + { + "name": "password", + "ordinal": 1, + "type_info": "Text" + } + ], + "nullable": [ + false, + false + ], + "parameters": { + "Left": [ + "Text" + ] + } + }, + "query": "SELECT name, password FROM librepages_users WHERE name = ($1)" + }, + "279b5ae27935279b06d2799eef2da6a316324a05d23ba7a729c608c70168c496": { + "describe": { + "columns": [], + "nullable": [], + "parameters": { + "Left": [ + "Varchar", + "Text" + ] + } + }, + "query": "UPDATE librepages_users set name = $1\n WHERE name = $2" + }, + "5c5d774bde06c0ab83c3616a56a28f12dfd9c546cbaac9f246d3b350c587823e": { + "describe": { + "columns": [], + "nullable": [], + "parameters": { + "Left": [ + "Text" + ] + } + }, + "query": "DELETE FROM librepages_users WHERE name = ($1)" + }, + "6a557f851d4f47383b864085093beb0954e79779f21b655978f07e285281e0ac": { + "describe": { + "columns": [], + "nullable": [], + "parameters": { + "Left": [ + "Varchar", + "Text" + ] + } + }, + "query": "UPDATE librepages_users set email = $1\n WHERE name = $2" + }, + "8735b654bc261571e6a5908d55a8217474c76bdff7f3cbcc71500a0fe13249db": { + "describe": { + "columns": [ + { + "name": "exists", + "ordinal": 0, + "type_info": "Bool" + } + ], + "nullable": [ + null + ], + "parameters": { + "Left": [ + "Text" + ] + } + }, + "query": "SELECT EXISTS (SELECT 1 from librepages_users WHERE email = $1)" + }, + "924e756de5544cece865a10a7e136ecc6126e3a603947264cc7899387c18c819": { + "describe": { + "columns": [], + "nullable": [], + "parameters": { + "Left": [ + "Text", + "Text" + ] + } + }, + "query": "UPDATE librepages_users set password = $1\n WHERE name = $2" + }, + "aa0e7d72a4542f28db816a8cc20e3f7b778e90e3cbe982a5a24af0b682adaf7d": { + "describe": { + "columns": [], + "nullable": [], + "parameters": { + "Left": [ + "Varchar", + "Text", + "Varchar" + ] + } + }, + "query": "insert into librepages_users\n (name , password, email) values ($1, $2, $3)" + }, + "b48c77db6e663d97df44bf9ec2ee92fd3e02f2dcbcdbd1d491e09fab2da68494": { + "describe": { + "columns": [ + { + "name": "name", + "ordinal": 0, + "type_info": "Varchar" + }, + { + "name": "password", + "ordinal": 1, + "type_info": "Text" + } + ], + "nullable": [ + false, + false + ], + "parameters": { + "Left": [ + "Text" + ] + } + }, + "query": "SELECT name, password FROM librepages_users WHERE email = ($1)" + }, + "bdd4d2a1b0b97ebf8ed61cfd120b40146fbf3ea9afb5cd0e03c9d29860b6a26b": { + "describe": { + "columns": [ + { + "name": "exists", + "ordinal": 0, + "type_info": "Bool" + } + ], + "nullable": [ + null + ], + "parameters": { + "Left": [ + "Text" + ] + } + }, + "query": "SELECT EXISTS (SELECT 1 from librepages_users WHERE name = $1)" + }, + "ced69a08729ffb906e8971dbdce6a8d4197bc9bb8ccd7c58b3a88eb7be73fc2e": { + "describe": { + "columns": [ + { + "name": "email", + "ordinal": 0, + "type_info": "Varchar" + } + ], + "nullable": [ + false + ], + "parameters": { + "Left": [ + "Text" + ] + } + }, + "query": "SELECT email FROM librepages_users WHERE name = $1" + } } \ No newline at end of file diff --git a/src/db.rs b/src/db.rs index f5e05f5..e4a1ba0 100644 --- a/src/db.rs +++ b/src/db.rs @@ -16,6 +16,7 @@ */ use std::str::FromStr; +use serde::{Deserialize, Serialize}; use sqlx::postgres::PgPoolOptions; use sqlx::types::time::OffsetDateTime; //use sqlx::types::Json; @@ -58,7 +59,7 @@ impl ConnectionOptions { .connect_with(connect_options) .await .unwrap() - //.map_err(|e| DBError::DBError(Box::new(e)))? + //.map_err(|e| ServiceError::ServiceError(Box::new(e)))? } Self::Existing(conn) => conn.0, @@ -78,7 +79,7 @@ impl Database { .run(&self.pool) .await .unwrap(); - //.map_err(|e| DBError::DBError(Box::new(e)))?; + //.map_err(|e| ServiceError::ServiceError(Box::new(e)))?; Ok(()) } @@ -91,6 +92,200 @@ impl Database { false } } + + /// register a new user + pub async fn register(&self, p: &Register<'_>) -> ServiceResult<()> { + sqlx::query!( + "insert into librepages_users + (name , password, email) values ($1, $2, $3)", + &p.username, + &p.hash, + &p.email, + ) + .execute(&self.pool) + .await + .map_err(map_register_err)?; + Ok(()) + } + + /// delete a user + pub async fn delete_user(&self, username: &str) -> ServiceResult<()> { + sqlx::query!("DELETE FROM librepages_users WHERE name = ($1)", username) + .execute(&self.pool) + .await + .map_err(|e| map_row_not_found_err(e, ServiceError::AccountNotFound))?; + Ok(()) + } + + /// check if username exists + pub async fn username_exists(&self, username: &str) -> ServiceResult { + let res = sqlx::query!( + "SELECT EXISTS (SELECT 1 from librepages_users WHERE name = $1)", + username, + ) + .fetch_one(&self.pool) + .await + .map_err(map_register_err)?; + + let mut resp = false; + if let Some(x) = res.exists { + resp = x; + } + + Ok(resp) + } + + /// get user email + pub async fn get_email(&self, username: &str) -> ServiceResult { + struct Email { + email: String, + } + + let res = sqlx::query_as!( + Email, + "SELECT email FROM librepages_users WHERE name = $1", + username + ) + .fetch_one(&self.pool) + .await + .map_err(|e| map_row_not_found_err(e, ServiceError::AccountNotFound))?; + Ok(res.email) + } + + /// check if email exists + pub async fn email_exists(&self, email: &str) -> ServiceResult { + let res = sqlx::query!( + "SELECT EXISTS (SELECT 1 from librepages_users WHERE email = $1)", + email + ) + .fetch_one(&self.pool) + .await + .map_err(map_register_err)?; + + let mut resp = false; + if let Some(x) = res.exists { + resp = x; + } + + Ok(resp) + } + + /// update a user's email + pub async fn update_email(&self, p: &UpdateEmail<'_>) -> ServiceResult<()> { + sqlx::query!( + "UPDATE librepages_users set email = $1 + WHERE name = $2", + &p.new_email, + &p.username, + ) + .execute(&self.pool) + .await + .map_err(|e| map_row_not_found_err(e, ServiceError::AccountNotFound))?; + + Ok(()) + } + + /// get a user's password + pub async fn get_password(&self, l: &Login<'_>) -> ServiceResult { + struct Password { + name: String, + password: String, + } + + let rec = match l { + Login::Username(u) => sqlx::query_as!( + Password, + r#"SELECT name, password FROM librepages_users WHERE name = ($1)"#, + u, + ) + .fetch_one(&self.pool) + .await + .map_err(|e| map_row_not_found_err(e, ServiceError::AccountNotFound))?, + Login::Email(e) => sqlx::query_as!( + Password, + r#"SELECT name, password FROM librepages_users WHERE email = ($1)"#, + e, + ) + .fetch_one(&self.pool) + .await + .map_err(|e| map_row_not_found_err(e, ServiceError::AccountNotFound))?, + }; + + let res = NameHash { + hash: rec.password, + username: rec.name, + }; + + Ok(res) + } + + /// update user's password + async fn update_password(&self, p: &NameHash) -> ServiceResult<()> { + sqlx::query!( + "UPDATE librepages_users set password = $1 + WHERE name = $2", + &p.hash, + &p.username, + ) + .execute(&self.pool) + .await + .map_err(|e| map_row_not_found_err(e, ServiceError::AccountNotFound))?; + + Ok(()) + } + + /// update username + async fn update_username(&self, current: &str, new: &str) -> ServiceResult<()> { + sqlx::query!( + "UPDATE librepages_users set name = $1 + WHERE name = $2", + new, + current, + ) + .execute(&self.pool) + .await + .map_err(|e| map_row_not_found_err(e, ServiceError::AccountNotFound))?; + + Ok(()) + } +} + +#[derive(Clone, Debug, Deserialize, Serialize, Eq, PartialEq)] +/// Data required to register a new user +pub struct Register<'a> { + /// username of new user + pub username: &'a str, + /// hashed password of new use + pub hash: &'a str, + /// Optionally, email of new use + pub email: &'a str, +} + +#[derive(Clone, Debug, Deserialize, Serialize, Eq, PartialEq)] +/// data required to update them email of a user +pub struct UpdateEmail<'a> { + /// username of the user + pub username: &'a str, + /// new email address of the user + pub new_email: &'a str, +} + +#[derive(Clone, Debug, Deserialize, Serialize, Eq, PartialEq)] +/// types of credentials used as identifiers during login +pub enum Login<'a> { + /// username as login + Username(&'a str), + /// email as login + Email(&'a str), +} + +#[derive(Clone, Debug, Deserialize, Serialize, Eq, PartialEq)] +/// type encapsulating username and hashed password of a user +pub struct NameHash { + /// username + pub username: String, + /// hashed password + pub hash: String, } fn now_unix_time_stamp() -> OffsetDateTime { @@ -109,6 +304,40 @@ pub async fn get_db(settings: &crate::settings::Settings) -> Database { .unwrap() } +/// map custom row not found error to DB error +pub fn map_row_not_found_err(e: sqlx::Error, row_not_found: ServiceError) -> ServiceError { + if let sqlx::Error::RowNotFound = e { + row_not_found + } else { + map_register_err(e) + } +} + +/// map postgres errors to [ServiceError](ServiceError) types +fn map_register_err(e: sqlx::Error) -> ServiceError { + use sqlx::Error; + use std::borrow::Cow; + + if let Error::Database(err) = e { + if err.code() == Some(Cow::from("23505")) { + let msg = err.message(); + println!("{}", msg); + if msg.contains("librepages_users_name_key") { + ServiceError::UsernameTaken + } else if msg.contains("librepages_users_email_key") { + ServiceError::EmailTaken + } else { + log::error!("{}", msg); + ServiceError::InternalServerError + } + } else { + ServiceError::InternalServerError + } + } else { + ServiceError::InternalServerError + } +} + #[cfg(test)] mod tests { use super::*; @@ -127,5 +356,102 @@ mod tests { .await .unwrap(); assert!(db.ping().await); + + const EMAIL: &str = "postgresuser@foo.com"; + const EMAIL2: &str = "postgresuser2@foo.com"; + const NAME: &str = "postgresuser"; + const PASSWORD: &str = "pasdfasdfasdfadf"; + + db.migrate().await.unwrap(); + let p = super::Register { + username: NAME, + email: EMAIL, + hash: PASSWORD, + }; + + if db.username_exists(p.username).await.unwrap() { + db.delete_user(p.username).await.unwrap(); + assert!( + !db.username_exists(p.username).await.unwrap(), + "user is deleted so username shouldn't exist" + ); + } + + db.register(&p).await.unwrap(); + + assert!(matches!( + db.register(&p).await, + Err(ServiceError::UsernameTaken) + )); + + // testing get_password + + // with username + let name_hash = db.get_password(&Login::Username(p.username)).await.unwrap(); + assert_eq!(name_hash.hash, p.hash, "user password matches"); + + assert_eq!(name_hash.username, p.username, "username matches"); + + // with email + let mut name_hash = db.get_password(&Login::Email(p.email)).await.unwrap(); + assert_eq!(name_hash.hash, p.hash, "user password matches"); + assert_eq!(name_hash.username, p.username, "username matches"); + + // testing get_email + assert_eq!(db.get_email(p.username).await.unwrap(), p.email); + + // testing email exists + assert!( + db.email_exists(p.email).await.unwrap(), + "user is registered so email should exist" + ); + assert!( + db.username_exists(p.username).await.unwrap(), + "user is registered so username should exist" + ); + + // update password test. setting password = username + name_hash.hash = name_hash.username.clone(); + db.update_password(&name_hash).await.unwrap(); + + let name_hash = db.get_password(&Login::Username(p.username)).await.unwrap(); + assert_eq!( + name_hash.hash, p.username, + "user password matches with changed value" + ); + assert_eq!(name_hash.username, p.username, "username matches"); + + // update username to p.email + assert!( + !db.username_exists(p.email).await.unwrap(), + "user with p.email doesn't exist. pre-check to update username to p.email" + ); + db.update_username(p.username, p.email).await.unwrap(); + assert!( + db.username_exists(p.email).await.unwrap(), + "user with p.email exist post-update" + ); + + // testing update email + let update_email = UpdateEmail { + username: p.username, + new_email: EMAIL2, + }; + db.update_email(&update_email).await.unwrap(); + println!( + "null user email: {}", + db.email_exists(p.email).await.unwrap() + ); + assert!( + db.email_exists(p.email).await.unwrap(), + "user was with empty email but email is set; so email should exist" + ); + + // deleting user + db.delete_user(p.email).await.unwrap(); + assert!( + !db.username_exists(p.email).await.unwrap(), + "user is deleted so username shouldn't exist" + ); } } diff --git a/src/errors.rs b/src/errors.rs index e1085d3..509cc9c 100644 --- a/src/errors.rs +++ b/src/errors.rs @@ -16,6 +16,7 @@ */ //! Represents all the ways a trait can fail using this crate use std::convert::From; +use std::error::Error as StdError; use std::io::Error as FSErrorInner; use std::sync::Arc; @@ -126,6 +127,16 @@ pub enum ServiceError { #[display(fmt = "Branch {} not found", _0)] BranchNotFound(#[error(not(source))] String), + + /// Username is taken + #[display(fmt = "Username is taken")] + UsernameTaken, + /// Email is taken + #[display(fmt = "Email is taken")] + EmailTaken, + /// Account not found + #[display(fmt = "Account not found")] + AccountNotFound, } impl From for ServiceError { @@ -184,6 +195,10 @@ impl ResponseError for ServiceError { ServiceError::BadRequest(_) => StatusCode::BAD_REQUEST, ServiceError::GitError(_) => StatusCode::BAD_REQUEST, ServiceError::BranchNotFound(_) => StatusCode::CONFLICT, + + ServiceError::EmailTaken => StatusCode::BAD_REQUEST, + ServiceError::UsernameTaken => StatusCode::BAD_REQUEST, + ServiceError::AccountNotFound => StatusCode::NOT_FOUND, } } }