summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authormrw1593 <botahamec@outlook.com>2023-03-19 13:23:20 -0400
committermrw1593 <botahamec@outlook.com>2023-05-29 10:45:26 -0400
commit8ec105595db9d2957c7327112e7a0b63d9d59400 (patch)
treea53e2e2c375d2d7155c30058a69dd713be4e5905
parentf149374e2c6682ea5b9b1d692b001d6ab5faea4a (diff)
Create user
-rw-r--r--Cargo.lock15
-rw-r--r--Cargo.toml6
-rw-r--r--sqlx-data.json87
-rw-r--r--src/api/liveops.rs2
-rw-r--r--src/api/mod.rs2
-rw-r--r--src/api/users.rs62
-rw-r--r--src/main.rs3
-rw-r--r--src/models/mod.rs3
-rw-r--r--src/models/user.rs44
-rw-r--r--src/services/crypto.rs20
-rw-r--r--src/services/db.rs50
-rw-r--r--src/services/id.rs19
-rw-r--r--src/services/mod.rs1
13 files changed, 304 insertions, 10 deletions
diff --git a/Cargo.lock b/Cargo.lock
index ece075a..1fb82f4 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -1113,6 +1113,12 @@ dependencies = [
]
[[package]]
+name = "raise"
+version = "2.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5127de0ecc0dd007559117b06737ec010d7316513685c2d3adf2b8b8252ce589"
+
+[[package]]
name = "rand"
version = "0.8.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -1221,9 +1227,13 @@ version = "0.1.0"
dependencies = [
"actix-web",
"exun",
+ "raise",
"rand",
"rust-argon2",
+ "serde",
"sqlx",
+ "thiserror",
+ "uuid",
]
[[package]]
@@ -1741,6 +1751,11 @@ name = "uuid"
version = "1.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1674845326ee10d37ca60470760d4288a6f80f304007d92e5c53bab78c9cfd79"
+dependencies = [
+ "getrandom",
+ "rand",
+ "serde",
+]
[[package]]
name = "version_check"
diff --git a/Cargo.toml b/Cargo.toml
index c30af2e..0c40ad7 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -7,7 +7,11 @@ edition = "2021"
[dependencies]
actix-web = "4"
+serde = "1"
+thiserror = "1"
rust-argon2 = "1"
-rand = { version = "0.8", features = [ "small_rng" ] }
+uuid = { version = "1", features = [ "v4", "fast-rng", "serde" ] }
+raise = "2"
+rand = "0.8"
sqlx = { version = "0.6", features = [ "runtime-actix-rustls", "mysql", "uuid", "offline" ] }
exun = "0.1"
diff --git a/sqlx-data.json b/sqlx-data.json
index 4295bdb..b2c46e2 100644
--- a/sqlx-data.json
+++ b/sqlx-data.json
@@ -1,3 +1,88 @@
{
- "db": "MySQL"
+ "db": "MySQL",
+ "1f6464fd7ab12c16c6b3d003471fc316d5aca3ec33e6cd2ebfc73a45a50f5d09": {
+ "describe": {
+ "columns": [
+ {
+ "name": "e: bool",
+ "ordinal": 0,
+ "type_info": {
+ "char_set": 63,
+ "flags": {
+ "bits": 129
+ },
+ "max_size": 1,
+ "type": "LongLong"
+ }
+ }
+ ],
+ "nullable": [
+ false
+ ],
+ "parameters": {
+ "Right": 1
+ }
+ },
+ "query": "SELECT EXISTS(SELECT user_id FROM users WHERE email = ?) as \"e: bool\""
+ },
+ "2a874f8bf5448493f2724b31022f3f8ebb535de0d780c19bb11e70d0da3e12fb": {
+ "describe": {
+ "columns": [
+ {
+ "name": "e: bool",
+ "ordinal": 0,
+ "type_info": {
+ "char_set": 63,
+ "flags": {
+ "bits": 129
+ },
+ "max_size": 1,
+ "type": "LongLong"
+ }
+ }
+ ],
+ "nullable": [
+ false
+ ],
+ "parameters": {
+ "Right": 1
+ }
+ },
+ "query": "SELECT EXISTS(SELECT user_id FROM users WHERE username = ?) as \"e: bool\""
+ },
+ "e7915f4cc41910baa074655a175f10bec198b69c4a4876cface46c875a1985c8": {
+ "describe": {
+ "columns": [],
+ "nullable": [],
+ "parameters": {
+ "Right": 6
+ }
+ },
+ "query": "INSERT INTO users (user_id, username, email, password_hash, password_salt, password_version)\n\t\t\t\t\t VALUES (?, ?, ?, ?, ?, ?)"
+ },
+ "ef52d73fde91d8771b14607544be1c12985e7db6c902062a2addd3b367502745": {
+ "describe": {
+ "columns": [
+ {
+ "name": "e: bool",
+ "ordinal": 0,
+ "type_info": {
+ "char_set": 63,
+ "flags": {
+ "bits": 129
+ },
+ "max_size": 1,
+ "type": "LongLong"
+ }
+ }
+ ],
+ "nullable": [
+ false
+ ],
+ "parameters": {
+ "Right": 1
+ }
+ },
+ "query": "SELECT EXISTS(SELECT user_id FROM users WHERE user_id = ?) as \"e: bool\""
+ }
} \ No newline at end of file
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;