diff --git a/backend/Cargo.lock b/backend/Cargo.lock index f4ada34..12e9e0b 100644 --- a/backend/Cargo.lock +++ b/backend/Cargo.lock @@ -295,12 +295,31 @@ version = "0.13.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9e1b586273c5702936fe7b7d6896644d8be71e6314cfe09d3167c95f712589e8" +[[package]] +name = "base64" +version = "0.21.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4a4ddaa51a5bc52a6948f74c06d20aaaddb71924eab79b8c97a8c556e942d6a" + [[package]] name = "base64ct" version = "1.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8c3c1a368f70d6cf7302d78f8f7093da241fb8e8807c05cc9e51a125895a6d5b" +[[package]] +name = "bcrypt" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9df288bec72232f78c1ec5fe4e8f1d108aa0265476e93097593c803c8c02062a" +dependencies = [ + "base64 0.21.0", + "blowfish", + "getrandom", + "subtle", + "zeroize", +] + [[package]] name = "bigdecimal" version = "0.3.1" @@ -342,6 +361,16 @@ dependencies = [ "log", ] +[[package]] +name = "blowfish" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e412e2cd0f2b2d93e02543ceae7917b3c70331573df19ee046bcbc35e45e87d7" +dependencies = [ + "byteorder", + "cipher", +] + [[package]] name = "borsh" version = "0.10.3" @@ -455,6 +484,16 @@ dependencies = [ "winapi", ] +[[package]] +name = "cipher" +version = "0.4.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "773f3b9af64447d2ce9850330c473515014aa235e6a783b02db81ff39e4a3dad" +dependencies = [ + "crypto-common", + "inout", +] + [[package]] name = "clap" version = "3.2.25" @@ -554,6 +593,7 @@ name = "crabfit_backend" version = "1.1.0" dependencies = [ "axum", + "bcrypt", "chrono", "common", "dotenv", @@ -1119,6 +1159,15 @@ dependencies = [ "hashbrown 0.12.3", ] +[[package]] +name = "inout" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a0c10553d664a4d0bcff9f4215d0aac67a639cc68ef660840afe309b807bc9f5" +dependencies = [ + "generic-array", +] + [[package]] name = "instant" version = "0.1.12" @@ -2280,7 +2329,7 @@ checksum = "fa8241483a83a3f33aa5fff7e7d9def398ff9990b2752b6c6112b83c6d246029" dependencies = [ "ahash 0.7.6", "atoi", - "base64", + "base64 0.13.1", "bigdecimal", "bitflags", "byteorder", diff --git a/backend/Cargo.toml b/backend/Cargo.toml index 99a15f9..86fce04 100644 --- a/backend/Cargo.toml +++ b/backend/Cargo.toml @@ -20,3 +20,4 @@ regex = "1.8.1" tracing = "0.1.37" tracing-subscriber = "0.3.17" chrono = "0.4.24" +bcrypt = "0.14.0" diff --git a/backend/adaptors/sql/src/lib.rs b/backend/adaptors/sql/src/lib.rs index ac34104..74419d1 100644 --- a/backend/adaptors/sql/src/lib.rs +++ b/backend/adaptors/sql/src/lib.rs @@ -71,17 +71,23 @@ impl Adaptor for SqlAdaptor { } async fn upsert_person(&self, event_id: String, person: Person) -> Result { - Ok(person::ActiveModel { - name: Set(person.name), + let data = person::ActiveModel { + name: Set(person.name.clone()), password_hash: Set(person.password_hash), created_at: Set(person.created_at.naive_utc()), availability: Set(serde_json::to_value(person.availability).unwrap_or(json!([]))), - event_id: Set(event_id), - } - .save(&self.db) - .await? - .try_into_model()? - .into()) + event_id: Set(event_id.clone()), + }; + + Ok( + match person::Entity::find_by_id((event_id, person.name)) + .one(&self.db) + .await? + { + Some(_) => data.update(&self.db).await?.try_into_model()?.into(), + None => data.insert(&self.db).await?.try_into_model()?.into(), + }, + ) } async fn get_event(&self, id: String) -> Result, Self::Error> { diff --git a/backend/src/errors.rs b/backend/src/errors.rs index f4cb7f8..47719e0 100644 --- a/backend/src/errors.rs +++ b/backend/src/errors.rs @@ -4,7 +4,7 @@ use common::adaptor::Adaptor; pub enum ApiError { AdaptorError(A::Error), NotFound, - // NotAuthorized, + NotAuthorized, } // Define what the error types above should return @@ -16,7 +16,7 @@ impl IntoResponse for ApiError { StatusCode::INTERNAL_SERVER_ERROR.into_response() } ApiError::NotFound => StatusCode::NOT_FOUND.into_response(), - // ApiError::NotAuthorized => StatusCode::UNAUTHORIZED.into_response(), + ApiError::NotAuthorized => StatusCode::UNAUTHORIZED.into_response(), } } } diff --git a/backend/src/main.rs b/backend/src/main.rs index bf3da57..d9c05fb 100644 --- a/backend/src/main.rs +++ b/backend/src/main.rs @@ -39,9 +39,10 @@ async fn main() { let app = Router::new() .route("/", get(get_root)) .route("/stats", get(get_stats)) - .route("/event/:event_id", get(get_event)) .route("/event", post(create_event)) + .route("/event/:event_id", get(get_event)) .route("/event/:event_id/people", get(get_people)) + .route("/event/:event_id/people/:person_name", get(get_person)) .with_state(shared_state); let addr = SocketAddr::from(([127, 0, 0, 1], 3000)); diff --git a/backend/src/payloads.rs b/backend/src/payloads.rs index 27e9334..655c19f 100644 --- a/backend/src/payloads.rs +++ b/backend/src/payloads.rs @@ -58,3 +58,8 @@ impl From for PersonResponse { } } } + +#[derive(Deserialize)] +pub struct PersonInput { + pub password: Option, +} diff --git a/backend/src/routes/get_person.rs b/backend/src/routes/get_person.rs new file mode 100644 index 0000000..348acf5 --- /dev/null +++ b/backend/src/routes/get_person.rs @@ -0,0 +1,87 @@ +use axum::{ + extract::{self, Path}, + Json, +}; +use common::{adaptor::Adaptor, person::Person}; + +use crate::{ + errors::ApiError, + payloads::{ApiResult, PersonInput, PersonResponse}, + State, +}; + +pub async fn get_person( + extract::State(state): State, + Path((event_id, person_name)): Path<(String, String)>, + input: Option>, +) -> ApiResult { + let adaptor = &state.lock().await.adaptor; + + // Get inputted password + let password = match input { + Some(Json(i)) => i.password, + None => None, + }; + + let existing_people = adaptor + .get_people(event_id.clone()) + .await + .map_err(ApiError::AdaptorError)?; + + // Event not found + if existing_people.is_none() { + return Err(ApiError::NotFound); + } + + // Check if the user already exists + let existing_person = existing_people + .unwrap() + .into_iter() + .find(|p| p.name == person_name); + + match existing_person { + // Login + Some(p) => { + // Verify password (if set) + if verify_password(&p, password) { + Ok(Json(p.into())) + } else { + Err(ApiError::NotAuthorized) + } + } + // Signup + None => { + // Update stats + adaptor + .increment_stat_person_count() + .await + .map_err(ApiError::AdaptorError)?; + + Ok(Json( + adaptor + .upsert_person( + event_id, + Person { + name: person_name, + password_hash: password + .map(|raw| bcrypt::hash(raw, 10).unwrap_or(String::from(""))), + created_at: chrono::offset::Utc::now(), + availability: vec![], + }, + ) + .await + .map_err(ApiError::AdaptorError)? + .into(), + )) + } + } +} + +fn verify_password(person: &Person, raw: Option) -> bool { + match &person.password_hash { + Some(hash) => bcrypt::verify(raw.unwrap_or(String::from("")), hash).unwrap_or(false), + // Specifically allow a user who doesn't have a password + // set to log in with or without any password input + None => true, + } +} diff --git a/backend/src/routes/mod.rs b/backend/src/routes/mod.rs index 1eb81d7..84f5044 100644 --- a/backend/src/routes/mod.rs +++ b/backend/src/routes/mod.rs @@ -9,3 +9,6 @@ pub use create_event::create_event; mod get_people; pub use get_people::get_people; + +mod get_person; +pub use get_person::get_person;