Almost all daos

This commit is contained in:
Franklin 2023-09-20 17:49:40 -04:00
parent 94502b0b54
commit 85bbef2121
25 changed files with 386 additions and 42 deletions

24
Cargo.lock generated
View File

@ -1613,30 +1613,6 @@ dependencies = [
"serde", "serde",
"sqlx", "sqlx",
"tokio", "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]] [[package]]

View File

@ -17,4 +17,3 @@ chrono = { version = "0.4", features = [ "serde" ] }
ring = "0.16.20" ring = "0.16.20"
data-encoding = "2.3.2" data-encoding = "2.3.2"
futures-util = "0.3" futures-util = "0.3"
uuid = { version = "1.3.0", features = ["v4", "fast-rng", "macro-diagnostics", "serde"] }

View File

@ -2,7 +2,9 @@ CREATE TABLE IF NOT EXISTS credential (
user_id INT NOT NULL, user_id INT NOT NULL,
credential_type VARCHAR NOT NULL, credential_type VARCHAR NOT NULL,
credential VARCHAR NOT NULL, credential VARCHAR NOT NULL,
validated BOOLEAN NOT NULL,
time_created TIMESTAMPTZ NOT NULL, time_created TIMESTAMPTZ NOT NULL,
last_updated TIMESTAMPTZ NOT NULL, last_updated TIMESTAMPTZ NOT NULL,
PRIMARY KEY(user_id, credential_type) PRIMARY KEY(user_id, credential_type),
UNIQUE(credential)
); );

22
src/dao/credential.rs Normal file
View File

@ -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<CredentialDto>, user_id: &i32) -> Result<Vec<Credential>, 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<Vec<Credential>, 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
}

View File

@ -1 +1,4 @@
pub mod pg_queries; pub mod pg_queries;
pub mod user;
pub mod credential;
pub mod token;

18
src/dao/token.rs Normal file
View File

@ -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<Token, Error> {
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<Token, Error> {
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
}

41
src/dao/user.rs Normal file
View File

@ -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<User, sqlx::Error> {
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<Option<User>, 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<User, sqlx::Error> {
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<Option<User>, sqlx::Error> {
sqlx::query_as(r#"
DELETE FROM user where id = $1 RETURNING *;
"#, )
.bind(user_id)
.fetch_optional(conn)
.await
}

View File

@ -1,5 +1,7 @@
use chrono::{DateTime, Utc}; use chrono::{DateTime, Utc};
use serde::{Serialize, Deserialize}; 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. /// 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 /// 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")] #[serde(rename_all = "camelCase")]
pub struct Credential { pub struct Credential {
pub user_id: i32, pub user_id: i32,
pub credential_type: CredentialType, pub credential_type: CredentialType,
pub credential: String, pub credential: String,
pub validated: bool,
pub time_created: DateTime<Utc>, pub time_created: DateTime<Utc>,
pub last_updated: DateTime<Utc>, pub last_updated: DateTime<Utc>,
} }
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,
}
}
}

View File

@ -2,8 +2,6 @@ use chrono::{DateTime, Utc};
use serde::{Serialize, Deserialize}; use serde::{Serialize, Deserialize};
use sqlx::FromRow; use sqlx::FromRow;
use super::credential::CredentialType;
#[derive(FromRow)] #[derive(FromRow)]
#[derive(Serialize, Deserialize, Debug, Default, Clone, PartialEq, Eq, PartialOrd, Ord)] #[derive(Serialize, Deserialize, Debug, Default, Clone, PartialEq, Eq, PartialOrd, Ord)]
pub struct User { pub struct User {

9
src/dto/credential.rs Normal file
View File

@ -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,
}

10
src/dto/hash_result.rs Normal file
View File

@ -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 }
}
}

View File

@ -1 +1,4 @@
pub mod users; pub mod users;
pub mod credential;
pub mod token;
pub mod hash_result;

15
src/dto/token.rs Normal file
View File

@ -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,
}

View File

@ -1,14 +1,20 @@
pub struct UsernameUserLoginPayload { use serde::{Deserialize, Serialize};
pub username: String, 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 password: String,
} }
pub struct EmailUserLoginPayload { #[derive(Serialize, Deserialize, Debug, Default, Clone, PartialEq, Eq, PartialOrd, Ord)]
pub email: String, #[serde(rename_all = "camelCase")]
pub password: String, pub struct UserRegisterPayload {
} pub credentials: Vec<CredentialDto>,
pub struct PhoneNumberUserLoginPayload {
pub phone_number: String,
pub password: String, pub password: String,
pub name: String,
} }

View File

@ -3,3 +3,5 @@ pub mod service;
pub mod utils; pub mod utils;
pub mod domain; pub mod domain;
pub mod dto; pub mod dto;
pub mod validation;
pub mod resources;

View File

@ -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.");

2
src/resources/mod.rs Normal file
View File

@ -0,0 +1,2 @@
pub mod error_messages;
pub mod variable_lengths;

View File

@ -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;

View File

@ -0,0 +1,2 @@
mod user;
mod token;

0
src/service/token.rs Normal file
View File

6
src/service/user.rs Normal file
View File

@ -0,0 +1,6 @@
use crate::dto::users::UserRegisterPayload;
pub async fn register_user(db_conn: &sqlx::PgPool, user: UserRegisterPayload) -> Result<(), ()> {
Ok(())
}

88
src/utils/hasher.rs Normal file
View File

@ -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<Vec<String>, 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<String> = 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))
}

View File

@ -0,0 +1 @@
mod hasher;

1
src/validation/mod.rs Normal file
View File

@ -0,0 +1 @@
pub mod user_validator;

View File

@ -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<ErrorResource>){
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<ErrorResource>){
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);
}
}