Added error constants, variable length constants, finished user creation & added password hashing with salts and verification
This commit is contained in:
parent
7a8847ed5b
commit
a71402aad0
|
@ -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"
|
|
@ -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"
|
|
@ -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
|
||||
)
|
|
@ -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
|
||||
)
|
|
@ -1,2 +0,0 @@
|
|||
-- This file should undo anything in `up.sql`
|
||||
DROP TABLE user
|
|
@ -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
|
||||
)
|
|
@ -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
|
||||
)
|
|
@ -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
|
||||
)
|
|
@ -0,0 +1,3 @@
|
|||
INSERT INTO token
|
||||
(id, user_id, time_created, last_updated, auth_token, refresh_token)
|
||||
values (NULL, ?, NOW(), NOW(), ?, ?)
|
|
@ -0,0 +1,3 @@
|
|||
INSERT INTO user
|
||||
(id, time_created, last_updated, email, name, password, salt) values
|
||||
(NULL, NOW(), NOW(), ?, ?, ?, ?)
|
|
@ -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());
|
||||
}
|
|
@ -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(())
|
||||
}
|
||||
}*/
|
|
@ -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>,
|
||||
}
|
|
@ -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() }
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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 }
|
||||
}
|
||||
}
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,3 @@
|
|||
pub mod user_dtos;
|
||||
pub mod message_resources_dtos;
|
||||
pub mod hash_dtos;
|
|
@ -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
|
||||
}
|
25
src/main.rs
25
src/main.rs
|
@ -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
|
||||
|
||||
}
|
||||
|
|
|
@ -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.");
|
|
@ -0,0 +1,2 @@
|
|||
pub mod error_messages;
|
||||
pub mod variable_lengths;
|
|
@ -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;
|
|
@ -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
|
||||
|
|
|
@ -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(())
|
||||
}
|
|
@ -1,6 +0,0 @@
|
|||
table! {
|
||||
user (id) {
|
||||
id -> Integer,
|
||||
name -> Varchar,
|
||||
}
|
||||
}
|
|
@ -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())
|
||||
}
|
|
@ -1 +1,2 @@
|
|||
pub mod env_util;
|
||||
pub mod env_util;
|
||||
pub mod hasher;
|
|
@ -0,0 +1 @@
|
|||
pub mod user_validator;
|
|
@ -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));
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue