diff --git a/Cargo.lock b/Cargo.lock index 429397b..81cb9df 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -70,6 +70,7 @@ name = "api" version = "0.1.0" dependencies = [ "anyhow", + "argon2", "axum", "chrono", "dotenv", @@ -93,6 +94,18 @@ dependencies = [ "derive_arbitrary", ] +[[package]] +name = "argon2" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c3610892ee6e0cbce8ae2700349fcf8f98adb0dbfbee85aec3c9179d29cc072" +dependencies = [ + "base64ct", + "blake2", + "cpufeatures", + "password-hash", +] + [[package]] name = "async-trait" version = "0.1.83" @@ -222,6 +235,15 @@ dependencies = [ "serde", ] +[[package]] +name = "blake2" +version = "0.10.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "46502ad458c9a52b69d4d4d32775c788b7a1b85e8bc9d482d92250fc0e3f8efe" +dependencies = [ + "digest", +] + [[package]] name = "block-buffer" version = "0.10.4" @@ -1039,6 +1061,17 @@ dependencies = [ "windows-targets 0.52.6", ] +[[package]] +name = "password-hash" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "346f04948ba92c43e8469c1ee6736c7563d71012b17d40745260fe106aac2166" +dependencies = [ + "base64ct", + "rand_core", + "subtle", +] + [[package]] name = "paste" version = "1.0.15" diff --git a/Cargo.toml b/Cargo.toml index 8075ce9..0b1cd9a 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -23,3 +23,4 @@ sqlx = { version = "0.8.2", features = [ uuid = "1.11.0" chrono = "0.4.39" dotenv = "0.15.0" +argon2 = "0.5.3" diff --git a/src/v1/auth/login.rs b/src/v1/auth/login.rs new file mode 100644 index 0000000..e76ccad --- /dev/null +++ b/src/v1/auth/login.rs @@ -0,0 +1,45 @@ +use crate::*; +use argon2::{ + password_hash::{PasswordHash, PasswordVerifier}, + Argon2, +}; +use sqlx::query; + +#[derive(Serialize, Deserialize, ToSchema)] +pub struct LoginBody { + username: String, + password: String, +} + +/// Login +#[utoipa::path(post, path = "/login", responses((status = OK, body = String)), tag = super::AUTH_TAG)] +pub async fn login( + State(state): State, + Json(body): Json, +) -> Result { + let user = query!( + "SELECT id, real_name, username, email, password_hash, is_admin, created_at + FROM users + WHERE username = $1", + body.username, + ) + .fetch_one(&*state.db) + .await?; + + let argon2 = Argon2::default(); + let hash = PasswordHash::new(&body.password).expect("Password hashing failed"); + + if !argon2 + .verify_password(body.password.as_bytes(), &hash) + .is_ok() + { + return Err(AppError::Error(Errors::Unauthorized)); + } + + // technically I should be using a JWT and returning it to the client + // since this will be a mobile app. + + // let jwt = ... + + Ok(user.id.to_string()) +} diff --git a/src/v1/auth/mod.rs b/src/v1/auth/mod.rs index 9d00c2c..e71ed2c 100644 --- a/src/v1/auth/mod.rs +++ b/src/v1/auth/mod.rs @@ -1,13 +1,15 @@ -use crate::state::AppState; +use crate::AppState; pub(super) use super::*; pub mod signup; +pub mod login; pub const AUTH_TAG: &str = "auth"; pub fn router(state: AppState) -> OpenApiRouter { OpenApiRouter::new() .routes(routes!(signup::signup)) + .routes(routes!(login::login)) .with_state(state) } diff --git a/src/v1/auth/signup.rs b/src/v1/auth/signup.rs index 2519716..863314f 100644 --- a/src/v1/auth/signup.rs +++ b/src/v1/auth/signup.rs @@ -1,6 +1,11 @@ use crate::*; use sqlx::query; +use argon2::{ + password_hash::{rand_core::OsRng, PasswordHasher, SaltString}, + Argon2, +}; + #[derive(Serialize, Deserialize, ToSchema)] pub struct SignupBody { real_name: String, @@ -10,11 +15,19 @@ pub struct SignupBody { } /// Sign up -#[utoipa::path(get, path = "/signup", responses((status = OK, body = String)), tag = super::AUTH_TAG)] +#[utoipa::path(post, path = "/signup", responses((status = OK, body = String)), tag = super::AUTH_TAG)] pub async fn signup( State(state): State, Json(body): Json, ) -> Result { + let salt = SaltString::generate(&mut OsRng); + let argon2 = Argon2::default(); + + let hash = argon2 + .hash_password(body.password.as_bytes(), &salt) + .expect("Password hashing failed") + .to_string(); + let user = query!( "INSERT INTO users (real_name, username, email, password_hash) VALUES ($1, $2, $3, $4) @@ -22,7 +35,7 @@ pub async fn signup( body.real_name, body.username, body.email, - body.password, + hash ) .fetch_one(&*state.db) .await?;