Added error constants, variable length constants, finished user creation & added password hashing with salts and verification

This commit is contained in:
franklinblanco 2022-06-27 15:09:14 -04:00
parent 7a8847ed5b
commit a71402aad0
33 changed files with 316 additions and 75 deletions

View File

@ -9,8 +9,8 @@ edition = "2021"
dotenv = "0.15.0"
serde = { version = "1.0", features = ["derive"] }
tokio = { version = "1", features = ["full"] }
#sqlx = { version = "0.5", features = [ "runtime-tokio-rustls", "mysql" ] }
diesel = { version = "1.4.4", features = ["mysql", "chrono"] }
diesel_migrations = { version = "1.4.0"}
sqlx = { version = "0.5", features = [ "runtime-tokio-rustls", "mysql", "chrono" ] }
actix-web = "4"
chrono = "0.4"
chrono = { version = "0.4", features = [ "serde" ] }
ring = "0.16.20"
data-encoding = "2.3.2"

View File

@ -1,5 +0,0 @@
# For documentation on how to configure this file,
# see diesel.rs/guides/configuring-diesel-cli
[print_schema]
file = "src/schema.rs"

View File

@ -0,0 +1,9 @@
CREATE TABLE IF NOT EXISTS user (
id INT AUTO_INCREMENT PRIMARY KEY,
time_created TIMESTAMP NOT NULL,
last_updated TIMESTAMP NOT NULL,
email VARCHAR(255) NOT NULL,
name VARCHAR(255) NOT NULL,
password VARCHAR(255) NOT NULL,
salt VARCHAR(255) NOT NULL
)

View File

@ -0,0 +1,8 @@
CREATE TABLE IF NOT EXISTS token (
id INT AUTO_INCREMENT PRIMARY KEY,
user_id INT NOT NULL,
time_created TIMESTAMP NOT NULL,
last_updated TIMESTAMP NOT NULL,
auth_token VARCHAR(255) NOT NULL,
refresh_token VARCHAR(255) NOT NULL
)

View File

@ -1,2 +0,0 @@
-- This file should undo anything in `up.sql`
DROP TABLE user

View File

@ -1,5 +0,0 @@
-- Your SQL goes here
CREATE TABLE IF NOT EXISTS user (
id INT AUTO_INCREMENT PRIMARY KEY,
name VARCHAR(255) NOT NULL
)

View File

@ -0,0 +1,8 @@
CREATE TABLE IF NOT EXISTS token (
id INT AUTO_INCREMENT PRIMARY KEY,
user_id INT NOT NULL,
time_created TIMESTAMP NOT NULL,
last_updated TIMESTAMP NOT NULL,
auth_token VARCHAR(255) NOT NULL,
refresh_token VARCHAR(255) NOT NULL
)

View File

@ -0,0 +1,9 @@
CREATE TABLE IF NOT EXISTS user (
id INT AUTO_INCREMENT PRIMARY KEY,
time_created TIMESTAMP NOT NULL,
last_updated TIMESTAMP NOT NULL,
email VARCHAR(255) NOT NULL,
name VARCHAR(255) NOT NULL,
password VARCHAR(255) NOT NULL,
salt VARCHAR(255) NOT NULL
)

View File

@ -0,0 +1,3 @@
INSERT INTO token
(id, user_id, time_created, last_updated, auth_token, refresh_token)
values (NULL, ?, NOW(), NOW(), ?, ?)

View File

0
sql/schema/user/find.sql Normal file
View File

View File

@ -0,0 +1,3 @@
INSERT INTO user
(id, time_created, last_updated, email, name, password, salt) values
(NULL, NOW(), NOW(), ?, ?, ?, ?)

View File

View File

@ -1,10 +1,8 @@
use std::collections::HashMap;
use diesel::{MysqlConnection, Connection, ConnectionError};
use sqlx::{MySqlConnection, Connection};
use crate::embedded_migrations;
pub fn start_database_connection(env_vars: &HashMap<String, String>) -> Result<MysqlConnection, ConnectionError>{
pub async fn start_database_connection(env_vars: &HashMap<String, String>) -> Result<MySqlConnection, sqlx::Error>{
let db_user = match env_vars.get("DB_USER") {
Some(str) => str,
None => panic!("DB_USER env var not found")
@ -22,12 +20,11 @@ pub fn start_database_connection(env_vars: &HashMap<String, String>) -> Result<M
None => panic!("DB_DATABASE_NAME env var not found")
};
let formatted_db_url = &format!("mysql://{db_user}:{db_pass}@{db_host}/{db_database_name}");
MysqlConnection::establish(formatted_db_url)
sqlx::MySqlConnection::connect(&formatted_db_url).await
}
pub fn run_all_migrations(conn: &mut MysqlConnection){
match embedded_migrations::run(conn){
Ok(()) => {println!("{}", "Successfully ran migrations.")}
Err(e) => {panic!("Error happened while trying to run migrations Error: {}", e)}
pub async fn run_all_migrations(conn: &mut MySqlConnection){
match sqlx::migrate!("./migrations").run(conn).await {
Ok(()) => {println!("{}", "Successfully ran migrations.")},
Err(error) => {panic!("{error}")}
}
//TODO: log output like this: embedded_migrations::run_with_output(&connection, &mut std::io::stdout());
}

View File

@ -1,10 +1,16 @@
use diesel::prelude::*;
use sqlx::{MySqlConnection, mysql::MySqlQueryResult};
use crate::{schema::user, r#do::user::User};
use crate::r#do::user::User;
pub fn _insert_user(conn: &mut MysqlConnection, user: User) -> Result<usize, diesel::result::Error>{
diesel::insert_into(user::table).values(&user).execute(conn)
pub async fn insert_user(conn: &mut MySqlConnection, user_to_insert: &User) -> Result<MySqlQueryResult, sqlx::Error>{
sqlx::query_file!("sql/schema/user/insert.sql",
user_to_insert.email, user_to_insert.name, user_to_insert.password, user_to_insert.salt)
.execute(conn).await
}
pub fn _find_user_by_id(_conn: &mut MysqlConnection, _id: i32) -> Result<(), ()>{
//pub async fn _update_user(conn: &mut MySqlConnection, user_to_modify: &User) -> Result<(), sqlx::Error>{
// Ok(())
//}
/*pub async fn find_user_by_id(_conn: &mut MySqlConnection, _id: i32) -> Result<(), ()>{
//println!("{:?}", user::select(user, (id, name, time_created)).load::<User>(_conn));
Ok(())
}
}*/

View File

@ -1,10 +1,7 @@
use std::collections::HashMap;
use diesel::MysqlConnection;
use sqlx::MySqlConnection;
pub struct SharedStateObj{
pub db_conn: MysqlConnection,
pub db_conn: MySqlConnection,
pub env_vars: HashMap<String, String>,
}

View File

@ -1,11 +1,35 @@
use diesel::{Queryable, Insertable};
use serde::Serialize;
use chrono::{NaiveDateTime};
use serde::{Serialize, Deserialize};
use crate::schema::*;
use crate::dto::user_dtos::UserForCreationDto;
#[derive(Serialize, Queryable, Insertable)]
#[table_name = "user"]
#[derive(Serialize, Deserialize, Debug)]
pub struct User{
pub id: i32,
pub name: String
pub time_created: Option<NaiveDateTime>,
pub last_updated: Option<NaiveDateTime>,
pub email: String,
pub name: String,
pub password: String,
pub salt: String
}
impl User {
pub fn _new() -> User {
User { id: 0,
time_created: None, // This will be automatically generated from the database
last_updated: None, // This will be automatically generated from the database
email: "".to_string(),
name:"".to_string(),
password:"".to_string(),
salt: "".to_string() }
}
pub fn new_for_creation(incoming_user: &UserForCreationDto) -> User{
User { id: 0,
time_created: None, // This will be automatically generated from the database
last_updated: None, // This will be automatically generated from the database
email: incoming_user.email.to_string(),
name: incoming_user.name.to_string(),
password: incoming_user.password.to_string(),
salt: "".to_string() }
}
}

11
src/dto/hash_dtos.rs Normal file
View File

@ -0,0 +1,11 @@
pub struct HashResult{
pub salt: String,
pub hash: String
}
impl HashResult{
pub fn new(salt: String, hash: String) -> HashResult{
HashResult { salt, hash }
}
}

View File

@ -0,0 +1,23 @@
use serde::Serialize;
#[derive(Serialize)]
pub struct MessageResourceDto{
key: String,
message: String
}
impl MessageResourceDto{
pub fn _new(key: String, message: String) -> MessageResourceDto{
MessageResourceDto{
key,
message
}
}
pub fn new_from_error_message(error: (&str, &str)) -> MessageResourceDto{
MessageResourceDto {
key: error.0.to_string(),
message: error.1.to_string()
}
}
}

3
src/dto/mod.rs Normal file
View File

@ -0,0 +1,3 @@
pub mod user_dtos;
pub mod message_resources_dtos;
pub mod hash_dtos;

18
src/dto/user_dtos.rs Normal file
View File

@ -0,0 +1,18 @@
use serde::{Serialize, Deserialize};
#[derive(Serialize, Deserialize, Debug)]
pub struct UserForCreationDto{
pub email: String,
pub password: String,
pub name: String
}
#[derive(Serialize, Deserialize, Debug)]
pub struct UserForLoginDto{
pub email: String,
pub password: String
}
#[derive(Serialize, Deserialize, Debug)]
pub struct UserForAuthenticationDto{
pub email: String,
pub token: String
}

View File

@ -1,19 +1,12 @@
mod r#do; mod dao;
mod routes; mod service;
mod util; pub mod schema;
mod util; mod dto;
mod validation; mod resources;
use r#do::shared_state::SharedStateObj;
use util::env_util;
use routes::main_router::{start_all_routes, after_startup_fn};
use dao::main_dao::{self, run_all_migrations};
// Include diesel_migrations so embed_migrations! macro can be called.
#[macro_use]
extern crate diesel_migrations;
#[macro_use]
extern crate diesel;
// Run all migrations inside the path
embed_migrations!();
use dao::{main_dao::{self, run_all_migrations}};
#[tokio::main]
@ -23,17 +16,25 @@ async fn main() -> Result<(), std::io::Error> {
let env_vars = env_util::get_dot_env_map();
// Start database
let mut db_conn = match main_dao::start_database_connection(&env_vars) {
let mut db_conn = match main_dao::start_database_connection(&env_vars).await {
Ok(conn) => conn,
Err(e) => panic!("Failure starting the database. Reason: {}", e)
};
/*match insert_user(&mut db_conn,
&User{ id: 1, name: "s".to_string(),
time_created: Some(chrono::Utc::now().naive_utc()) }).await {
Ok(()) => {},
Err(e) => {panic!("ERROR MYSQL {}", e)}
}
*/
// Run all migrations
run_all_migrations(&mut db_conn);
run_all_migrations(&mut db_conn).await;
// Put db connection and env variables in shared state
let shared_state_obj = SharedStateObj {db_conn, env_vars };
// Pass shared state to server and start it
start_all_routes(&after_startup_fn, shared_state_obj).await
}

View File

@ -0,0 +1,8 @@
// This file stores all the error messages
// Template: pub const ERROR_KEY_OR_NAME: (&str, &str) = ("ERROR.KEY", "ERROR VALUE");
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_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.");

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,9 @@
pub const MIN_EMAIL_LENGTH: u16 = 4;
pub const MAX_EMAIL_LENGTH: u16 = 254;
pub const MIN_NAME_LENGTH: u16 = 4;
pub const MAX_NAME_LENGTH: u16 = 254;
pub const MIN_PASSWORD_LENGTH: u16 = 8;
pub const MAX_PASSWORD_LENGTH: u16 = 128;

View File

@ -33,17 +33,19 @@ pub async fn start_all_routes(after_startup_fn_call: &dyn Fn(), state: SharedSta
let db_conn_state = web::Data::new(Mutex::new(state.db_conn));
let env_vars_state = web::Data::new(Mutex::new(state.env_vars.clone()));
// Start server
// Start server code that turns into a future to be executed below
let server_future = HttpServer::new( move || {
App::new()
// Define routes & pass in shared state
.app_data(db_conn_state.clone())
.app_data(env_vars_state.clone())
.service(user_routes::get_user_from_db)
.service(user_routes::create_user)
//.service(user_routes::get_user_from_db)
})
.bind((host_addr, host_port))?
.run();
// Actual server start and after startup call
let (server_start_result, _after_startup_value) =
tokio::join!(server_future, async {after_startup_fn_call();});
return server_start_result; // Return server

View File

@ -1,17 +1,50 @@
use std::sync::Mutex;
use actix_web::{get, web::{self, Path, Data}, HttpResponse, post};
use diesel::MysqlConnection;
use actix_web::{web::{self, Data}, HttpResponse, post};
use sqlx::{MySqlConnection};
use crate::{r#do::user::User, dao::user_dao::_insert_user};
use crate::{r#do::user::User, dao::user_dao::{insert_user}, dto::{user_dtos::UserForCreationDto, message_resources_dtos::MessageResourceDto}, validation::user_validator, util::hasher};
#[get("/user/{id}")]
pub async fn get_user_from_db(id: Path<i32>, _data: Data<Mutex<MysqlConnection>>) -> HttpResponse {
_insert_user(&mut _data.lock().unwrap(), User { id: *id, name: "nigga".to_string() });
HttpResponse::Ok().json(web::Json(User {id: *id, name: "nigga".to_string()}))
}
/*#[get("/user/{id}")]
pub async fn get_user_from_db(id: Path<i32>, db_conn: Data<Mutex<MySqlConnection>>) -> HttpResponse {
match find_user_by_id(&mut db_conn.lock().unwrap(), *id).await{
Ok(MySqlQueryResult)
}
HttpResponse::Ok().json(web::Json("ss"))
}*/
#[post("/user")]
pub async fn create_user() -> HttpResponse {
HttpResponse::Ok().json(web::Json(""))
pub async fn create_user(incoming_user: web::Json<UserForCreationDto>, db_conn: Data<Mutex<MySqlConnection>>) -> HttpResponse {
let mut message_resources: Vec<MessageResourceDto> = Vec::new();
// Get user object from json
let incoming_user_obj = incoming_user.0;
// Transform userdto to user domain obj
let mut user_to_insert = User::new_for_creation(
&incoming_user_obj
);
// Validate user
user_validator::validate_user_for_creation(incoming_user_obj, &mut message_resources);
// If validation gave any errors blow up and send them back to the client
if message_resources.len() > 0 { return HttpResponse::BadRequest().json(web::Json(message_resources)); }
// Get salt and hashed password from hashing function then give the results to the user
let hash_result = hasher::hash_password(&user_to_insert.password);
user_to_insert.password = hash_result.hash;
user_to_insert.salt = hash_result.salt;
// Try to insert user in DB
match insert_user(&mut db_conn.lock().unwrap(), &user_to_insert).await{
Ok(_resultrs) => {},
Err(error) => {
println!("Error while inserting user in database from create_user method. Log: {}", error);
return HttpResponse::InternalServerError().json(web::Json(()))
}};
// TODO: Create token and send it back.
// All good? Send an OK!
HttpResponse::Ok().body(())
}

View File

@ -1,6 +0,0 @@
table! {
user (id) {
id -> Integer,
name -> Varchar,
}
}

52
src/util/hasher.rs Normal file
View File

@ -0,0 +1,52 @@
use std::{num::NonZeroU32};
use data_encoding::HEXUPPER;
use ring::{digest, rand::{SecureRandom, SystemRandom}, pbkdf2, error::Unspecified};
use crate::dto::hash_dtos::HashResult;
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(100_000).unwrap();
let rng = SystemRandom::new();
// Create empty 64-bit 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 a tuple containing the salt and the hash
HashResult::new(HEXUPPER.encode(&salt), HEXUPPER.encode(&pbkdf2_hash))
}
fn _verify_password_hash(input_password: &String, salt: &String, actual_hash: &String) -> Result<(), Unspecified>{
// Get output length from a sha512 hash
const CREDENTIAL_LEN: usize = digest::SHA512_OUTPUT_LEN;
let n_iter = NonZeroU32::new(100_000).unwrap();
// Verify the user-inputted password hashed with the salt is the same.
pbkdf2::verify(
pbkdf2::PBKDF2_HMAC_SHA512,
n_iter,
salt.as_bytes(),
input_password.as_bytes(),
actual_hash.as_bytes())
}

View File

@ -1 +1,2 @@
pub mod env_util;
pub mod hasher;

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

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

View File

@ -0,0 +1,31 @@
use crate::{
dto::{ user_dtos::UserForCreationDto, message_resources_dtos::MessageResourceDto },
resources::{ variable_lengths::*, error_messages::* }
};
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_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()
}
// User dto SHOULD die here.
pub fn validate_user_for_creation(user: UserForCreationDto, message_resources: &mut Vec<MessageResourceDto>){
if validate_user_email(&user.email) {
message_resources.push(MessageResourceDto::new_from_error_message(ERROR_INVALID_EMAIL));
}
if validate_user_name(&user.name) {
message_resources.push(MessageResourceDto::new_from_error_message(ERROR_INVALID_NAME));
}
if validate_user_password(&user.password) {
message_resources.push(MessageResourceDto::new_from_error_message(ERROR_INVALID_PASSWORD));
}
}