diff --git a/backend/adaptors/sql/src/lib.rs b/backend/adaptors/sql/src/lib.rs index 74419d1..0a9d017 100644 --- a/backend/adaptors/sql/src/lib.rs +++ b/backend/adaptors/sql/src/lib.rs @@ -80,7 +80,7 @@ impl Adaptor for SqlAdaptor { }; Ok( - match person::Entity::find_by_id((event_id, person.name)) + match person::Entity::find_by_id((person.name, event_id)) .one(&self.db) .await? { @@ -156,7 +156,16 @@ impl SqlAdaptor { // Connect to the database let db = Database::connect(&connection_string).await.unwrap(); - println!("Connected to database at {}", connection_string); + println!( + "{} Connected to database at {}", + match db { + DatabaseConnection::SqlxMySqlPoolConnection(_) => "🐬", + DatabaseConnection::SqlxPostgresPoolConnection(_) => "🐘", + DatabaseConnection::SqlxSqlitePoolConnection(_) => "🪶", + DatabaseConnection::Disconnected => panic!("Failed to connect"), + }, + connection_string + ); // Setup tables Migrator::up(&db, None).await.unwrap(); diff --git a/backend/src/main.rs b/backend/src/main.rs index d9c05fb..3ec7bf7 100644 --- a/backend/src/main.rs +++ b/backend/src/main.rs @@ -2,7 +2,7 @@ use std::{net::SocketAddr, sync::Arc}; use axum::{ extract, - routing::{get, post}, + routing::{get, patch, post}, Router, Server, }; use routes::*; @@ -43,11 +43,15 @@ async fn main() { .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)) + .route("/event/:event_id/people/:person_name", patch(update_person)) .with_state(shared_state); let addr = SocketAddr::from(([127, 0, 0, 1], 3000)); - println!("Crab Fit API listening at http://{} in {} mode", addr, MODE); + println!( + "🦀 Crab Fit API listening at http://{} in {} mode", + addr, MODE + ); Server::bind(&addr) .serve(app.into_make_service()) .await diff --git a/backend/src/payloads.rs b/backend/src/payloads.rs index 655c19f..2bfe430 100644 --- a/backend/src/payloads.rs +++ b/backend/src/payloads.rs @@ -1,5 +1,5 @@ use axum::Json; -use common::{event::Event, person::Person}; +use common::{event::Event, person::Person, stats::Stats}; use serde::{Deserialize, Serialize}; use crate::errors::ApiError; @@ -19,7 +19,7 @@ pub struct EventResponse { pub name: String, pub times: Vec, pub timezone: String, - pub created: i64, + pub created_at: i64, } impl From for EventResponse { @@ -29,24 +29,33 @@ impl From for EventResponse { name: value.name, times: value.times, timezone: value.timezone, - created: value.created_at.timestamp(), + created_at: value.created_at.timestamp(), } } } #[derive(Serialize)] -#[serde(rename_all(serialize = "camelCase"))] pub struct StatsResponse { pub event_count: i32, pub person_count: i32, pub version: String, } +impl From for StatsResponse { + fn from(value: Stats) -> Self { + Self { + event_count: value.event_count, + person_count: value.person_count, + version: env!("CARGO_PKG_VERSION").to_string(), + } + } +} + #[derive(Serialize)] pub struct PersonResponse { pub name: String, pub availability: Vec, - pub created: i64, + pub created_at: i64, } impl From for PersonResponse { @@ -54,12 +63,18 @@ impl From for PersonResponse { Self { name: value.name, availability: value.availability, - created: value.created_at.timestamp(), + created_at: value.created_at.timestamp(), } } } #[derive(Deserialize)] -pub struct PersonInput { +pub struct GetPersonInput { pub password: Option, } + +#[derive(Deserialize)] +pub struct UpdatePersonInput { + pub password: Option, + pub availability: Vec, +} diff --git a/backend/src/routes/get_event.rs b/backend/src/routes/get_event.rs index 98ad2a6..5ce0ca1 100644 --- a/backend/src/routes/get_event.rs +++ b/backend/src/routes/get_event.rs @@ -22,13 +22,7 @@ pub async fn get_event( .map_err(ApiError::AdaptorError)?; match event { - Some(event) => Ok(Json(EventResponse { - id: event.id, - name: event.name, - times: event.times, - timezone: event.timezone, - created: event.created_at.timestamp(), - })), + Some(event) => Ok(Json(event.into())), None => Err(ApiError::NotFound), } } diff --git a/backend/src/routes/get_person.rs b/backend/src/routes/get_person.rs index 348acf5..0e00df3 100644 --- a/backend/src/routes/get_person.rs +++ b/backend/src/routes/get_person.rs @@ -6,14 +6,14 @@ use common::{adaptor::Adaptor, person::Person}; use crate::{ errors::ApiError, - payloads::{ApiResult, PersonInput, PersonResponse}, + payloads::{ApiResult, GetPersonInput, PersonResponse}, State, }; pub async fn get_person( extract::State(state): State, Path((event_id, person_name)): Path<(String, String)>, - input: Option>, + input: Option>, ) -> ApiResult { let adaptor = &state.lock().await.adaptor; @@ -77,7 +77,7 @@ pub async fn get_person( } } -fn verify_password(person: &Person, raw: Option) -> bool { +pub 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 diff --git a/backend/src/routes/get_stats.rs b/backend/src/routes/get_stats.rs index 71a0d81..46d9efc 100644 --- a/backend/src/routes/get_stats.rs +++ b/backend/src/routes/get_stats.rs @@ -12,9 +12,5 @@ pub async fn get_stats(extract::State(state): State) -> ApiResult let stats = adaptor.get_stats().await.map_err(ApiError::AdaptorError)?; - Ok(Json(StatsResponse { - event_count: stats.event_count, - person_count: stats.person_count, - version: env!("CARGO_PKG_VERSION").to_string(), - })) + Ok(Json(stats.into())) } diff --git a/backend/src/routes/mod.rs b/backend/src/routes/mod.rs index 84f5044..4881408 100644 --- a/backend/src/routes/mod.rs +++ b/backend/src/routes/mod.rs @@ -12,3 +12,6 @@ pub use get_people::get_people; mod get_person; pub use get_person::get_person; + +mod update_person; +pub use update_person::update_person; diff --git a/backend/src/routes/update_person.rs b/backend/src/routes/update_person.rs new file mode 100644 index 0000000..99b3804 --- /dev/null +++ b/backend/src/routes/update_person.rs @@ -0,0 +1,59 @@ +use axum::{ + extract::{self, Path}, + Json, +}; +use common::{adaptor::Adaptor, person::Person}; + +use crate::{ + errors::ApiError, + payloads::{ApiResult, PersonResponse, UpdatePersonInput}, + State, +}; + +use super::get_person::verify_password; + +pub async fn update_person( + extract::State(state): State, + Path((event_id, person_name)): Path<(String, String)>, + Json(input): Json, +) -> ApiResult { + let adaptor = &state.lock().await.adaptor; + + 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 exists + let existing_person = existing_people + .unwrap() + .into_iter() + .find(|p| p.name == person_name) + .ok_or(ApiError::NotFound)?; + + // Verify password (if set) + if !verify_password(&existing_person, input.password) { + return Err(ApiError::NotAuthorized); + } + + Ok(Json( + adaptor + .upsert_person( + event_id, + Person { + name: existing_person.name, + password_hash: existing_person.password_hash, + created_at: existing_person.created_at, + availability: input.availability, + }, + ) + .await + .map_err(ApiError::AdaptorError)? + .into(), + )) +}