diff options
| author | mrw1593 <botahamec@outlook.com> | 2023-03-19 13:23:20 -0400 |
|---|---|---|
| committer | mrw1593 <botahamec@outlook.com> | 2023-05-29 10:45:26 -0400 |
| commit | 8ec105595db9d2957c7327112e7a0b63d9d59400 (patch) | |
| tree | a53e2e2c375d2d7155c30058a69dd713be4e5905 /src | |
| parent | f149374e2c6682ea5b9b1d692b001d6ab5faea4a (diff) | |
Create user
Diffstat (limited to 'src')
| -rw-r--r-- | src/api/liveops.rs | 2 | ||||
| -rw-r--r-- | src/api/mod.rs | 2 | ||||
| -rw-r--r-- | src/api/users.rs | 62 | ||||
| -rw-r--r-- | src/main.rs | 3 | ||||
| -rw-r--r-- | src/models/mod.rs | 3 | ||||
| -rw-r--r-- | src/models/user.rs | 44 | ||||
| -rw-r--r-- | src/services/crypto.rs | 20 | ||||
| -rw-r--r-- | src/services/db.rs | 50 | ||||
| -rw-r--r-- | src/services/id.rs | 19 | ||||
| -rw-r--r-- | src/services/mod.rs | 1 |
10 files changed, 198 insertions, 8 deletions
diff --git a/src/api/liveops.rs b/src/api/liveops.rs index ff44107..2dda41d 100644 --- a/src/api/liveops.rs +++ b/src/api/liveops.rs @@ -7,5 +7,5 @@ async fn ping() -> HttpResponse { } pub fn service() -> Scope { - web::scope("liveops/").service(ping) + web::scope("liveops").service(ping) } diff --git a/src/api/mod.rs b/src/api/mod.rs index f934d0e..7becfbd 100644 --- a/src/api/mod.rs +++ b/src/api/mod.rs @@ -1,3 +1,5 @@ mod liveops; +mod users; pub use liveops::service as liveops; +pub use users::service as users; diff --git a/src/api/users.rs b/src/api/users.rs new file mode 100644 index 0000000..5e409fd --- /dev/null +++ b/src/api/users.rs @@ -0,0 +1,62 @@ +use actix_web::http::{header, StatusCode}; +use actix_web::{post, web, HttpResponse, ResponseError, Scope}; +use raise::yeet; +use serde::Deserialize; +use sqlx::MySqlPool; +use thiserror::Error; + +use crate::models::User; +use crate::services::crypto::PasswordHash; +use crate::services::db::{new_user, username_is_used}; +use crate::services::id::new_user_id; + +#[derive(Clone, Deserialize)] +struct CreateUser { + username: Box<str>, + password: Box<str>, +} + +#[derive(Debug, Clone, Hash, Error)] +#[error("An account with the given username already exists.")] +struct CreateUserError { + username: Box<str>, +} + +impl ResponseError for CreateUserError { + fn status_code(&self) -> StatusCode { + StatusCode::CONFLICT + } +} + +#[post("")] +async fn create_user( + body: web::Json<CreateUser>, + conn: web::Data<MySqlPool>, +) -> Result<HttpResponse, CreateUserError> { + let conn = conn.get_ref(); + + let user_id = new_user_id(conn).await.unwrap(); + let username = body.username.clone(); + let password = PasswordHash::new(&body.password).unwrap(); + + if username_is_used(conn, &body.username).await.unwrap() { + yeet!(CreateUserError { username }); + } + + let user = User { + user_id, + username, + password, + }; + + new_user(conn, user).await.unwrap(); + + let response = HttpResponse::Created() + .insert_header((header::LOCATION, format!("users/{user_id}"))) + .finish(); + Ok(response) +} + +pub fn service() -> Scope { + web::scope("users").service(create_user) +} diff --git a/src/main.rs b/src/main.rs index dc0f9a7..7cc694d 100644 --- a/src/main.rs +++ b/src/main.rs @@ -2,6 +2,7 @@ use actix_web::{web::Data, App, HttpServer}; use exun::RawUnexpected; mod api; +mod models; mod services; use services::*; @@ -9,10 +10,12 @@ use services::*; #[actix_web::main] async fn main() -> Result<(), RawUnexpected> { let sql_pool = db::initialize("password_database", "dbuser", "Demo1234").await?; + HttpServer::new(move || { App::new() .app_data(Data::new(sql_pool.clone())) .service(api::liveops()) + .service(api::users()) }) .shutdown_timeout(1) .bind(("127.0.0.1", 8080))? diff --git a/src/models/mod.rs b/src/models/mod.rs new file mode 100644 index 0000000..4a9be81 --- /dev/null +++ b/src/models/mod.rs @@ -0,0 +1,3 @@ +mod user; + +pub use user::User; diff --git a/src/models/user.rs b/src/models/user.rs new file mode 100644 index 0000000..f5fd20e --- /dev/null +++ b/src/models/user.rs @@ -0,0 +1,44 @@ +use std::hash::Hash; + +use uuid::Uuid; + +use crate::services::crypto::PasswordHash; + +#[derive(Debug, Clone)] +pub struct User { + pub user_id: Uuid, + pub username: Box<str>, + pub password: PasswordHash, +} + +impl PartialEq for User { + fn eq(&self, other: &Self) -> bool { + self.user_id == other.user_id + } +} + +impl Eq for User {} + +impl Hash for User { + fn hash<H: std::hash::Hasher>(&self, state: &mut H) { + state.write_u128(self.user_id.as_u128()) + } +} + +impl User { + pub fn username(&self) -> &str { + &self.username + } + + pub fn password_hash(&self) -> &[u8] { + self.password.hash() + } + + pub fn password_salt(&self) -> &[u8] { + self.password.salt() + } + + pub fn password_version(&self) -> u8 { + self.password.version() + } +} diff --git a/src/services/crypto.rs b/src/services/crypto.rs index 11a5149..580e83a 100644 --- a/src/services/crypto.rs +++ b/src/services/crypto.rs @@ -10,7 +10,7 @@ static PEPPER: [u8; 16] = [ /// The configuration used for hashing and verifying passwords static CONFIG: argon2::Config<'_> = argon2::Config { - hash_length: 256, + hash_length: 32, lanes: 4, mem_cost: 5333, time_cost: 4, @@ -27,13 +27,12 @@ static CONFIG: argon2::Config<'_> = argon2::Config { pub struct PasswordHash { hash: Box<[u8]>, salt: Box<[u8]>, + version: u8, } impl Hash for PasswordHash { fn hash<H: std::hash::Hasher>(&self, state: &mut H) { - for byte in self.hash.iter() { - state.write_u8(*byte) - } + state.write(&self.hash) } } @@ -47,14 +46,19 @@ impl PasswordHash { let hash = hash_raw(password, &salt, &CONFIG)?.into_boxed_slice(); - Ok(Self { hash, salt }) + Ok(Self { + hash, + salt, + version: 0, + }) } /// Create this structure from a given hash and salt - pub fn from_hash_salt(hash: &[u8], salt: &[u8]) -> Self { + pub fn from_fields(hash: &[u8], salt: &[u8], version: u8) -> Self { Self { hash: Box::from(hash), salt: Box::from(salt), + version, } } @@ -68,6 +72,10 @@ impl PasswordHash { &self.salt } + pub fn version(&self) -> u8 { + self.version + } + /// Check if the given password is the one that was hashed pub fn check_password(&self, password: &str) -> Result<bool, RawUnexpected> { Ok(verify_raw( diff --git a/src/services/db.rs b/src/services/db.rs index a8e4918..efa9584 100644 --- a/src/services/db.rs +++ b/src/services/db.rs @@ -1,8 +1,56 @@ use exun::*; -use sqlx::MySqlPool; +use sqlx::{query, query_scalar, Executor, MySql, MySqlPool}; +use uuid::Uuid; + +use crate::models::User; /// Intialize the connection pool pub async fn initialize(db: &str, user: &str, password: &str) -> Result<MySqlPool, RawUnexpected> { let url = format!("mysql://{user}:{password}@localhost/{db}"); MySqlPool::connect(&url).await.unexpect() } + +pub async fn user_id_exists<'c>( + conn: impl Executor<'c, Database = MySql>, + id: Uuid, +) -> Result<bool, RawUnexpected> { + let exists = query_scalar!( + r#"SELECT EXISTS(SELECT user_id FROM users WHERE user_id = ?) as "e: bool""#, + id + ) + .fetch_one(conn) + .await?; + + Ok(exists) +} + +pub async fn username_is_used<'c>( + conn: impl Executor<'c, Database = MySql>, + username: &str, +) -> Result<bool, RawUnexpected> { + let exists = query_scalar!( + r#"SELECT EXISTS(SELECT user_id FROM users WHERE username = ?) as "e: bool""#, + username + ) + .fetch_one(conn) + .await?; + + Ok(exists) +} + +pub async fn new_user<'c>( + conn: impl Executor<'c, Database = MySql>, + user: User, +) -> Result<sqlx::mysql::MySqlQueryResult, sqlx::Error> { + query!( + r"INSERT INTO users (user_id, username, password_hash, password_salt, password_version) + VALUES (?, ?, ?, ?, ?)", + user.user_id, + user.username(), + user.password_hash(), + user.password_salt(), + user.password_version() + ) + .execute(conn) + .await +} diff --git a/src/services/id.rs b/src/services/id.rs new file mode 100644 index 0000000..7970c60 --- /dev/null +++ b/src/services/id.rs @@ -0,0 +1,19 @@ +use exun::RawUnexpected; +use sqlx::{Executor, MySql}; +use uuid::Uuid; + +use super::db; + +/// Create a unique user id, handling duplicate ID's +pub async fn new_user_id<'c>( + conn: impl Executor<'c, Database = MySql> + Clone, +) -> Result<Uuid, RawUnexpected> { + let uuid = loop { + let uuid = Uuid::new_v4(); + if !db::user_id_exists(conn.clone(), uuid).await? { + break uuid; + } + }; + + Ok(uuid) +} diff --git a/src/services/mod.rs b/src/services/mod.rs index 7163603..57146d8 100644 --- a/src/services/mod.rs +++ b/src/services/mod.rs @@ -1,2 +1,3 @@ pub mod crypto; pub mod db; +pub mod id; |
