diff --git a/Cargo.lock b/Cargo.lock index 6d96370..9ceefb3 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1613,30 +1613,6 @@ dependencies = [ "serde", "sqlx", "tokio", - "uuid", -] - -[[package]] -name = "uuid" -version = "1.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "79daa5ed5740825c40b389c5e50312b9c86df53fccd33f281df655642b43869d" -dependencies = [ - "getrandom", - "rand", - "serde", - "uuid-macro-internal", -] - -[[package]] -name = "uuid-macro-internal" -version = "1.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f7e1ba1f333bd65ce3c9f27de592fcbc256dafe3af2717f56d7c87761fbaccf4" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.27", ] [[package]] diff --git a/Cargo.toml b/Cargo.toml index b88f13b..62c7bab 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -16,5 +16,4 @@ sqlx = { version = "0.7", features = [ "runtime-tokio", "tls-rustls", "postgres" chrono = { version = "0.4", features = [ "serde" ] } ring = "0.16.20" data-encoding = "2.3.2" -futures-util = "0.3" -uuid = { version = "1.3.0", features = ["v4", "fast-rng", "macro-diagnostics", "serde"] } \ No newline at end of file +futures-util = "0.3" \ No newline at end of file diff --git a/migrations/3_credential.sql b/migrations/3_credential.sql index 8ffaf1c..1c5a8d3 100644 --- a/migrations/3_credential.sql +++ b/migrations/3_credential.sql @@ -2,7 +2,9 @@ CREATE TABLE IF NOT EXISTS credential ( user_id INT NOT NULL, credential_type VARCHAR NOT NULL, credential VARCHAR NOT NULL, + validated BOOLEAN NOT NULL, time_created TIMESTAMPTZ NOT NULL, last_updated TIMESTAMPTZ NOT NULL, - PRIMARY KEY(user_id, credential_type) + PRIMARY KEY(user_id, credential_type), + UNIQUE(credential) ); \ No newline at end of file diff --git a/src/dao/credential.rs b/src/dao/credential.rs new file mode 100644 index 0000000..785c7be --- /dev/null +++ b/src/dao/credential.rs @@ -0,0 +1,22 @@ +use chrono::Utc; +use sqlx::{Error, PgPool}; +use crate::domain::credential::Credential; +use crate::dto::credential::CredentialDto; + +pub async fn insert_credentials(conn: &PgPool, credentials: Vec, user_id: &i32) -> Result, Error> { + let insert_query_base = r#"INSERT INTO credential + (user_id, credential_type, credential, validated, time_created, last_updated) + VALUES ($1, $2, $3, $4, $5, $5) RETURNING user_id, credential_type as "credential_type: _", credential, validated, time_created, last_updated"#; + let mut persisted_credentials = Vec::new(); + for credential_dto in credentials { + let persisted_credential: Credential = sqlx::query_as(insert_query_base) + .bind(user_id).bind(credential_dto.credential_type).bind(credential_dto.credential).bind(false).bind(Utc::now()) + .fetch_one(conn).await?; + persisted_credentials.push(persisted_credential); + } + Ok(persisted_credentials) +} + +pub async fn fetch_user_credentials(conn: &PgPool, user_id: &i32) -> Result, Error> { + sqlx::query_as(r#"SELECT user_id, credential_type as "credential_type: _", credential, validated, time_created, last_updated FROM credential WHERE user_id = $1 "#).bind(user_id).fetch_all(conn).await +} \ No newline at end of file diff --git a/src/dao/mod.rs b/src/dao/mod.rs index 8eec167..73678b3 100644 --- a/src/dao/mod.rs +++ b/src/dao/mod.rs @@ -1 +1,4 @@ -pub mod pg_queries; \ No newline at end of file +pub mod pg_queries; +pub mod user; +pub mod credential; +pub mod token; \ No newline at end of file diff --git a/src/dao/token.rs b/src/dao/token.rs new file mode 100644 index 0000000..362df2f --- /dev/null +++ b/src/dao/token.rs @@ -0,0 +1,18 @@ +use chrono::Utc; +use sqlx::{Error, PgPool}; +use crate::domain::token::Token; + +pub async fn insert_token(conn: &PgPool, token: Token) -> Result { + sqlx::query_as(r#"INSERT INTO token ( + user_id, auth_token, refresh_token, time_created, last_updated) VALUES ($1, $2, $3, $4, $4) RETURNING *;"#) + .bind(token.user_id).bind(token.auth_token).bind(token.refresh_token).bind(token.time_created) + .fetch_one(conn).await +} + +pub async fn update_token(conn: &PgPool, token_id: &i32, auth_token: String) -> Result { + sqlx::query_as(r#"UPDATE token set + auth_token = $2, last_updated + WHERE id = $1 RETURNING *;"#) + .bind(token_id).bind(auth_token).bind(Utc::now()) + .fetch_one(conn).await +} \ No newline at end of file diff --git a/src/dao/user.rs b/src/dao/user.rs new file mode 100644 index 0000000..7c4b138 --- /dev/null +++ b/src/dao/user.rs @@ -0,0 +1,41 @@ +use sqlx::{PgPool, query_as, query_as_with}; +use crate::domain::user::User; + +pub async fn insert_user(conn: &PgPool, user: User) -> Result { + sqlx::query_as(r#" + INSERT INTO user (name, password, salt, time_created, last_updated) + VALUES ($1, $2, $3, $4, $4) RETURNING *; + "#) + .bind(user.name).bind(user.password).bind(user.salt).bind(user.time_created) + .fetch_one(conn) + .await +} + +pub async fn get_user_with_id(conn: &PgPool, user_id: &i32) -> Result, sqlx::Error> { + sqlx::query_as(r#" + SELECT * FROM user where id = $1; + "#, ) + .bind(user_id) + .fetch_optional(conn) + .await +} + +pub async fn update_user(conn: &PgPool, user: User) -> Result { + sqlx::query_as(r#" + UPDATE user SET + name = $2, password = $3, salt = $4, last_updated = $5 + WHERE id = $1 RETURNING *; + "#, ) + .bind(user.id).bind(user.name).bind(user.password).bind(user.salt).bind(user.last_updated) + .fetch_one(conn) + .await +} + +pub async fn delete_user(conn: &PgPool, user_id: &i32) -> Result, sqlx::Error> { + sqlx::query_as(r#" + DELETE FROM user where id = $1 RETURNING *; + "#, ) + .bind(user_id) + .fetch_optional(conn) + .await +} \ No newline at end of file diff --git a/src/domain/credential.rs b/src/domain/credential.rs index fa93b2b..373aa01 100644 --- a/src/domain/credential.rs +++ b/src/domain/credential.rs @@ -1,5 +1,7 @@ use chrono::{DateTime, Utc}; use serde::{Serialize, Deserialize}; +use sqlx::FromRow; +use crate::resources::variable_lengths::{MAX_EMAIL_LENGTH, MAX_NAME_LENGTH, MAX_PHONE_NUMBER_LENGTH, MAX_USERNAME_LENGTH, MIN_EMAIL_LENGTH, MIN_PHONE_NUMBER_LENGTH, MIN_USERNAME_LENGTH}; /// Is used in the user struct to signal what type of credential will be used in the credential Column. @@ -13,12 +15,30 @@ pub enum CredentialType { } /// Can only have one per user per cred_type -#[derive(Serialize, Deserialize, Debug, Default, Clone, PartialEq, Eq, PartialOrd, Ord)] +#[derive(Serialize, Deserialize, Debug, Default, Clone, PartialEq, Eq, PartialOrd, Ord, FromRow)] #[serde(rename_all = "camelCase")] pub struct Credential { pub user_id: i32, pub credential_type: CredentialType, pub credential: String, + pub validated: bool, pub time_created: DateTime, pub last_updated: DateTime, +} + +impl CredentialType { + pub fn get_max_length(&self) -> usize { + match self { + CredentialType::PhoneNumber => MAX_PHONE_NUMBER_LENGTH, + CredentialType::Email => MAX_EMAIL_LENGTH, + CredentialType::Username => MAX_USERNAME_LENGTH, + } + } + pub fn get_min_length(&self) -> usize { + match self { + CredentialType::PhoneNumber => MIN_PHONE_NUMBER_LENGTH, + CredentialType::Email => MIN_EMAIL_LENGTH, + CredentialType::Username => MIN_USERNAME_LENGTH, + } + } } \ No newline at end of file diff --git a/src/domain/user.rs b/src/domain/user.rs index 6acb777..39564f1 100644 --- a/src/domain/user.rs +++ b/src/domain/user.rs @@ -2,8 +2,6 @@ use chrono::{DateTime, Utc}; use serde::{Serialize, Deserialize}; use sqlx::FromRow; -use super::credential::CredentialType; - #[derive(FromRow)] #[derive(Serialize, Deserialize, Debug, Default, Clone, PartialEq, Eq, PartialOrd, Ord)] pub struct User { diff --git a/src/dto/credential.rs b/src/dto/credential.rs new file mode 100644 index 0000000..d981925 --- /dev/null +++ b/src/dto/credential.rs @@ -0,0 +1,9 @@ +use serde::{Deserialize, Serialize}; +use crate::domain::credential::CredentialType; + +#[derive(Serialize, Deserialize, Debug, Default, Clone, PartialEq, Eq, PartialOrd, Ord)] +#[serde(rename_all = "camelCase")] +pub struct CredentialDto { + pub credential: String, + pub credential_type: CredentialType, +} \ No newline at end of file diff --git a/src/dto/hash_result.rs b/src/dto/hash_result.rs new file mode 100644 index 0000000..b39b38c --- /dev/null +++ b/src/dto/hash_result.rs @@ -0,0 +1,10 @@ +pub struct HashResult { + pub salt: String, + pub hash: String +} + +impl HashResult{ + pub fn new(salt: String, hash: String) -> HashResult{ + HashResult { salt, hash } + } +} \ No newline at end of file diff --git a/src/dto/mod.rs b/src/dto/mod.rs index e3146d3..f51a488 100644 --- a/src/dto/mod.rs +++ b/src/dto/mod.rs @@ -1 +1,4 @@ -pub mod users; \ No newline at end of file +pub mod users; +pub mod credential; +pub mod token; +pub mod hash_result; \ No newline at end of file diff --git a/src/dto/token.rs b/src/dto/token.rs new file mode 100644 index 0000000..213e9a6 --- /dev/null +++ b/src/dto/token.rs @@ -0,0 +1,15 @@ +use serde::{Deserialize, Serialize}; + +#[derive(Serialize, Deserialize, Debug, Default, Clone, PartialEq, Eq, PartialOrd, Ord)] +#[serde(rename_all = "camelCase")] +pub struct AuthenticateUserDto { + pub id: i32, + pub auth_token: String, +} + +#[derive(Serialize, Deserialize, Debug, Default, Clone, PartialEq, Eq, PartialOrd, Ord)] +#[serde(rename_all = "camelCase")] +pub struct RefreshAuthTokenForUserDto { + pub id: i32, + pub refresh_token: String, +} \ No newline at end of file diff --git a/src/dto/users.rs b/src/dto/users.rs index fea7510..917dfce 100644 --- a/src/dto/users.rs +++ b/src/dto/users.rs @@ -1,14 +1,20 @@ -pub struct UsernameUserLoginPayload { - pub username: String, +use serde::{Deserialize, Serialize}; +use crate::domain::credential::CredentialType; +use crate::dto::credential::CredentialDto; + +/// Used for logging in when you don't have a token. +#[derive(Serialize, Deserialize, Debug, Default, Clone, PartialEq, Eq, PartialOrd, Ord)] +#[serde(rename_all = "camelCase")] +pub struct UserLoginPayload { + pub credential: String, + pub credential_type: CredentialType, pub password: String, } -pub struct EmailUserLoginPayload { - pub email: String, - pub password: String, -} - -pub struct PhoneNumberUserLoginPayload { - pub phone_number: String, +#[derive(Serialize, Deserialize, Debug, Default, Clone, PartialEq, Eq, PartialOrd, Ord)] +#[serde(rename_all = "camelCase")] +pub struct UserRegisterPayload { + pub credentials: Vec, pub password: String, + pub name: String, } \ No newline at end of file diff --git a/src/lib.rs b/src/lib.rs index 0c25ce1..26150af 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -2,4 +2,6 @@ pub mod dao; pub mod service; pub mod utils; pub mod domain; -pub mod dto; \ No newline at end of file +pub mod dto; +pub mod validation; +pub mod resources; \ No newline at end of file diff --git a/src/resources/error_messages.rs b/src/resources/error_messages.rs new file mode 100644 index 0000000..dd5ccc4 --- /dev/null +++ b/src/resources/error_messages.rs @@ -0,0 +1,29 @@ +// This file stores all the error messages +// Template: pub const ERROR_KEY_OR_NAME: (&str, &str) = ("ERROR.KEY", "ERROR VALUE"); + +pub type ErrorResource<'a> = (&'a str, &'a str); +pub const ERROR_INVALID_EMAIL: (&str, &str) = ("ERROR.INVALID_EMAIL", "Invalid email. Needs to be at least 4 characters, at most 254 and correctly formatted."); + +pub const ERROR_INVALID_PHONE_NUMBER: (&str, &str) = ("ERROR.INVALID_PHONE_NUMBER", "Invalid Phone number. Needs to be 10 characters."); + +pub const ERROR_INVALID_USERNAME: (&str, &str) = ("ERROR.INVALID_USERNAME", "Invalid Username. "); + +pub const ERROR_INVALID_NAME: (&str, &str) = ("ERROR.INVALID_NAME", "Invalid name. Names should have at least 4 characters in length and at most 254."); + +pub const ERROR_INVALID_PASSWORD: (&str, &str) = ("ERROR.INVALID_PASSWORD", "Invalid password. Password should have at least 8 characters and at most 128."); + +pub const ERROR_USER_ALREADY_EXISTS: (&str, &str) = ("ERROR.USER_ALREADY_EXISTS", "A user with that email already exists."); + +pub const ERROR_USER_DOES_NOT_EXIST: (&str, &str) = ("ERROR.USER_DOES_NOT_EXIST", "User with this email does not exist."); + +pub const ERROR_PASSWORD_INCORRECT: (&str, &str) = ("ERROR.PASSWORD_INCORRECT", "The password you have entered is incorrect."); + +pub const ERROR_INVALID_TOKEN: (&str, &str) = ("ERROR.INVALID_TOKEN", "The token you have supplied is not formattable."); + +pub const ERROR_INCORRECT_TOKEN: (&str, &str) = ("ERROR.INCORRECT_TOKEN", "The token you have supplied does not belong to this user."); + +pub const ERROR_MISSING_TOKEN: (&str, &str) = ("ERROR.MISSING_TOKEN", "No token supplied."); + +pub const ERROR_EXPIRED_TOKEN: (&str, &str) = ("ERROR.EXPIRED_TOKEN", "The token you have supplied is expired."); + +pub const ERROR_CREATING_TOKEN: (&str, &str) = ("ERROR.CREATING_TOKEN", "The server had an error creating the auth tokens."); \ No newline at end of file diff --git a/src/resources/mod.rs b/src/resources/mod.rs new file mode 100644 index 0000000..76bc0b4 --- /dev/null +++ b/src/resources/mod.rs @@ -0,0 +1,2 @@ +pub mod error_messages; +pub mod variable_lengths; \ No newline at end of file diff --git a/src/resources/variable_lengths.rs b/src/resources/variable_lengths.rs new file mode 100644 index 0000000..9a6352b --- /dev/null +++ b/src/resources/variable_lengths.rs @@ -0,0 +1,15 @@ + +pub const MIN_EMAIL_LENGTH: usize = 4; +pub const MAX_EMAIL_LENGTH: usize = 254; + +pub const MIN_PHONE_NUMBER_LENGTH: usize = 8; +pub const MAX_PHONE_NUMBER_LENGTH: usize = 14; + +pub const MIN_USERNAME_LENGTH: usize = 3; +pub const MAX_USERNAME_LENGTH: usize = 64; + +pub const MIN_NAME_LENGTH: usize = 4; +pub const MAX_NAME_LENGTH: usize = 254; + +pub const MIN_PASSWORD_LENGTH: usize = 8; +pub const MAX_PASSWORD_LENGTH: usize = 128; \ No newline at end of file diff --git a/src/service/mod.rs b/src/service/mod.rs index e69de29..a7dcc69 100644 --- a/src/service/mod.rs +++ b/src/service/mod.rs @@ -0,0 +1,2 @@ +mod user; +mod token; \ No newline at end of file diff --git a/src/service/token.rs b/src/service/token.rs new file mode 100644 index 0000000..e69de29 diff --git a/src/service/user.rs b/src/service/user.rs new file mode 100644 index 0000000..c553bb1 --- /dev/null +++ b/src/service/user.rs @@ -0,0 +1,6 @@ +use crate::dto::users::UserRegisterPayload; + +pub async fn register_user(db_conn: &sqlx::PgPool, user: UserRegisterPayload) -> Result<(), ()> { + + Ok(()) +} \ No newline at end of file diff --git a/src/utils/hasher.rs b/src/utils/hasher.rs new file mode 100644 index 0000000..2c67056 --- /dev/null +++ b/src/utils/hasher.rs @@ -0,0 +1,88 @@ +use std::num::NonZeroU32; +use data_encoding::BASE64; +use ring::{digest, rand::{SecureRandom, SystemRandom}, pbkdf2}; +use tokio::task::JoinError; +use crate::dto::hash_result::HashResult; + +const SALT_ROUNDS: u32 = 1000; + +pub async fn generate_multiple_random_token_with_rng(amount: u8) -> Result, JoinError> { + // Get a new instance of a Random Number Generator + let rng = SystemRandom::new(); + + let mut tokens = Vec::with_capacity(amount.into()); + + for _i in 0 .. amount { + let cloned_rng = rng.clone(); + let future_token = async move { + let mut token_arr = [0u8; digest::SHA512_OUTPUT_LEN]; + match cloned_rng.fill(&mut token_arr){ + Ok(()) => {BASE64.encode(&token_arr)}, //TODO: Remove this panic, make your own error and fix this + Err(_e) => { panic!("Failed to generate random token for some reason.") } + }}; + tokens.push(tokio::spawn(future_token)); + } + + let all_tokens = futures_util::future::join_all(tokens).await; + let all_tokens_solved: Vec = all_tokens.into_iter().map(|result| match result { + Ok(string) => {string}, + Err(_e) => {"".to_string()} + }).rev().collect(); + + Ok(all_tokens_solved) +} + +pub fn hash_password_with_existing_salt(password: &String, input_salt: &String) -> HashResult{ + // Get output length from a sha512 hash + const CREDENTIAL_LEN: usize = digest::SHA512_OUTPUT_LEN; + let n_iter = NonZeroU32::new(SALT_ROUNDS).unwrap(); + + let salt = BASE64.decode(input_salt.as_bytes()).unwrap(); + + // Create empty 64-bit byte array for the hash + salt + let mut pbkdf2_hash = [0u8; CREDENTIAL_LEN]; + + // Fills byte array with hashed values + pbkdf2::derive( + pbkdf2::PBKDF2_HMAC_SHA512, + n_iter, + &salt, + password.as_bytes(), + &mut pbkdf2_hash, + ); + + // Return an object containing the salt and the hash + HashResult::new(BASE64.encode(&salt), BASE64.encode(&pbkdf2_hash)) +} + +pub fn hash_password(password: &String) -> HashResult{ + + // Get output length from a sha512 hash + const CREDENTIAL_LEN: usize = digest::SHA512_OUTPUT_LEN; + let n_iter = NonZeroU32::new(SALT_ROUNDS).unwrap(); + let rng = SystemRandom::new(); + + // Create empty 64-byte array for the salt + let mut salt = [0u8; CREDENTIAL_LEN]; + + // Fill array with random-generated salt + match rng.fill(&mut salt){ + Ok(()) => {}, + Err(_e) => {panic!("Failed to generate random salt for some reason.")} + } + + // Create empty 64-bit byte array for the hash + salt + let mut pbkdf2_hash = [0u8; CREDENTIAL_LEN]; + + // Fills byte array with hashed values + pbkdf2::derive( + pbkdf2::PBKDF2_HMAC_SHA512, + n_iter, + &salt, + password.as_bytes(), + &mut pbkdf2_hash, + ); + + // Return an object containing the salt and the hash + HashResult::new(BASE64.encode(&salt), BASE64.encode(&pbkdf2_hash)) +} \ No newline at end of file diff --git a/src/utils/mod.rs b/src/utils/mod.rs index e69de29..a6b30dd 100644 --- a/src/utils/mod.rs +++ b/src/utils/mod.rs @@ -0,0 +1 @@ +mod hasher; \ No newline at end of file diff --git a/src/validation/mod.rs b/src/validation/mod.rs new file mode 100644 index 0000000..4365395 --- /dev/null +++ b/src/validation/mod.rs @@ -0,0 +1 @@ +pub mod user_validator; \ No newline at end of file diff --git a/src/validation/user_validator.rs b/src/validation/user_validator.rs new file mode 100644 index 0000000..05c94a6 --- /dev/null +++ b/src/validation/user_validator.rs @@ -0,0 +1,76 @@ + + +use crate::{resources::{variable_lengths::{MAX_EMAIL_LENGTH, MIN_EMAIL_LENGTH, MIN_NAME_LENGTH, MAX_NAME_LENGTH, MIN_PASSWORD_LENGTH, MAX_PASSWORD_LENGTH}, error_messages::{ERROR_INVALID_NAME, ERROR_INVALID_EMAIL, ERROR_INVALID_PASSWORD, ERROR_INVALID_PHONE_NUMBER}}}; +use crate::domain::credential::CredentialType; +use crate::dto::users::{UserLoginPayload, UserRegisterPayload}; +use crate::resources::error_messages::{ERROR_INVALID_USERNAME, ErrorResource}; + + +fn validate_user_email(email: &String) -> bool { + email.len() >= MIN_EMAIL_LENGTH.into() && + email.len() <= MAX_EMAIL_LENGTH.into() && + email.contains('@') && + email.contains('.') +} +fn validate_user_phone_number(email: &String) -> bool { + email.len() >= CredentialType::get_max_length(&CredentialType::PhoneNumber) && + email.len() <= CredentialType::get_min_length(&CredentialType::PhoneNumber) +} + +fn validate_user_username(username: &String) -> bool { + username.len() >= CredentialType::get_max_length(&CredentialType::PhoneNumber) && + username.len() <= CredentialType::get_min_length(&CredentialType::PhoneNumber) +} +fn validate_user_name(name: &String) -> bool { + name.len() >= MIN_NAME_LENGTH.into() && + name.len() <= MAX_NAME_LENGTH.into() +} +fn validate_user_password(password: &String) -> bool { + password.len() >= MIN_PASSWORD_LENGTH.into() && + password.len() <= MAX_PASSWORD_LENGTH.into() +} + +pub fn validate_user_for_creation(user: &UserRegisterPayload, error_resources: &mut Vec){ + for credential_dto in user.credentials { + match credential_dto.credential_type { + CredentialType::Email => + if !validate_user_email(&credential_dto.credential) { + error_resources.push(ERROR_INVALID_EMAIL); + }, + CredentialType::PhoneNumber => + if !validate_user_phone_number(&credential_dto.credential) { + error_resources.push(ERROR_INVALID_PHONE_NUMBER); + }, + CredentialType::Username => + if !validate_user_username(&credential_dto.credential) { + error_resources.push(ERROR_INVALID_USERNAME); + }, + }; + } + + if !validate_user_name(&user.name) { + error_resources.push(ERROR_INVALID_NAME); + } + if !validate_user_password(&user.password) { + error_resources.push(ERROR_INVALID_PASSWORD); + } +} +pub fn validate_user_for_password_authentication(user: &UserLoginPayload, error_resources: &mut Vec){ + match user.credential_type { + CredentialType::Email => + if !validate_user_email(&user.credential) { + error_resources.push(ERROR_INVALID_EMAIL); + }, + CredentialType::PhoneNumber => + if !validate_user_phone_number(&user.credential) { + error_resources.push(ERROR_INVALID_PHONE_NUMBER); + }, + CredentialType::Username => + if !validate_user_username(&user.credential) { + error_resources.push(ERROR_INVALID_USERNAME); + }, + } + if !validate_user_password(&user.password) { + error_resources.push(ERROR_INVALID_PASSWORD); + } +} \ No newline at end of file