Add update_person route

This commit is contained in:
Ben Grant 2023-05-13 17:13:04 +10:00
parent d2a94b078c
commit cd18427d1b
8 changed files with 106 additions and 26 deletions

View file

@ -80,7 +80,7 @@ impl Adaptor for SqlAdaptor {
}; };
Ok( Ok(
match person::Entity::find_by_id((event_id, person.name)) match person::Entity::find_by_id((person.name, event_id))
.one(&self.db) .one(&self.db)
.await? .await?
{ {
@ -156,7 +156,16 @@ impl SqlAdaptor {
// Connect to the database // Connect to the database
let db = Database::connect(&connection_string).await.unwrap(); 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 // Setup tables
Migrator::up(&db, None).await.unwrap(); Migrator::up(&db, None).await.unwrap();

View file

@ -2,7 +2,7 @@ use std::{net::SocketAddr, sync::Arc};
use axum::{ use axum::{
extract, extract,
routing::{get, post}, routing::{get, patch, post},
Router, Server, Router, Server,
}; };
use routes::*; use routes::*;
@ -43,11 +43,15 @@ async fn main() {
.route("/event/:event_id", get(get_event)) .route("/event/:event_id", get(get_event))
.route("/event/:event_id/people", get(get_people)) .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", get(get_person))
.route("/event/:event_id/people/:person_name", patch(update_person))
.with_state(shared_state); .with_state(shared_state);
let addr = SocketAddr::from(([127, 0, 0, 1], 3000)); 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) Server::bind(&addr)
.serve(app.into_make_service()) .serve(app.into_make_service())
.await .await

View file

@ -1,5 +1,5 @@
use axum::Json; use axum::Json;
use common::{event::Event, person::Person}; use common::{event::Event, person::Person, stats::Stats};
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use crate::errors::ApiError; use crate::errors::ApiError;
@ -19,7 +19,7 @@ pub struct EventResponse {
pub name: String, pub name: String,
pub times: Vec<String>, pub times: Vec<String>,
pub timezone: String, pub timezone: String,
pub created: i64, pub created_at: i64,
} }
impl From<Event> for EventResponse { impl From<Event> for EventResponse {
@ -29,24 +29,33 @@ impl From<Event> for EventResponse {
name: value.name, name: value.name,
times: value.times, times: value.times,
timezone: value.timezone, timezone: value.timezone,
created: value.created_at.timestamp(), created_at: value.created_at.timestamp(),
} }
} }
} }
#[derive(Serialize)] #[derive(Serialize)]
#[serde(rename_all(serialize = "camelCase"))]
pub struct StatsResponse { pub struct StatsResponse {
pub event_count: i32, pub event_count: i32,
pub person_count: i32, pub person_count: i32,
pub version: String, pub version: String,
} }
impl From<Stats> 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)] #[derive(Serialize)]
pub struct PersonResponse { pub struct PersonResponse {
pub name: String, pub name: String,
pub availability: Vec<String>, pub availability: Vec<String>,
pub created: i64, pub created_at: i64,
} }
impl From<Person> for PersonResponse { impl From<Person> for PersonResponse {
@ -54,12 +63,18 @@ impl From<Person> for PersonResponse {
Self { Self {
name: value.name, name: value.name,
availability: value.availability, availability: value.availability,
created: value.created_at.timestamp(), created_at: value.created_at.timestamp(),
} }
} }
} }
#[derive(Deserialize)] #[derive(Deserialize)]
pub struct PersonInput { pub struct GetPersonInput {
pub password: Option<String>, pub password: Option<String>,
} }
#[derive(Deserialize)]
pub struct UpdatePersonInput {
pub password: Option<String>,
pub availability: Vec<String>,
}

View file

@ -22,13 +22,7 @@ pub async fn get_event<A: Adaptor>(
.map_err(ApiError::AdaptorError)?; .map_err(ApiError::AdaptorError)?;
match event { match event {
Some(event) => Ok(Json(EventResponse { Some(event) => Ok(Json(event.into())),
id: event.id,
name: event.name,
times: event.times,
timezone: event.timezone,
created: event.created_at.timestamp(),
})),
None => Err(ApiError::NotFound), None => Err(ApiError::NotFound),
} }
} }

View file

@ -6,14 +6,14 @@ use common::{adaptor::Adaptor, person::Person};
use crate::{ use crate::{
errors::ApiError, errors::ApiError,
payloads::{ApiResult, PersonInput, PersonResponse}, payloads::{ApiResult, GetPersonInput, PersonResponse},
State, State,
}; };
pub async fn get_person<A: Adaptor>( pub async fn get_person<A: Adaptor>(
extract::State(state): State<A>, extract::State(state): State<A>,
Path((event_id, person_name)): Path<(String, String)>, Path((event_id, person_name)): Path<(String, String)>,
input: Option<Json<PersonInput>>, input: Option<Json<GetPersonInput>>,
) -> ApiResult<PersonResponse, A> { ) -> ApiResult<PersonResponse, A> {
let adaptor = &state.lock().await.adaptor; let adaptor = &state.lock().await.adaptor;
@ -77,7 +77,7 @@ pub async fn get_person<A: Adaptor>(
} }
} }
fn verify_password(person: &Person, raw: Option<String>) -> bool { pub fn verify_password(person: &Person, raw: Option<String>) -> bool {
match &person.password_hash { match &person.password_hash {
Some(hash) => bcrypt::verify(raw.unwrap_or(String::from("")), hash).unwrap_or(false), Some(hash) => bcrypt::verify(raw.unwrap_or(String::from("")), hash).unwrap_or(false),
// Specifically allow a user who doesn't have a password // Specifically allow a user who doesn't have a password

View file

@ -12,9 +12,5 @@ pub async fn get_stats<A: Adaptor>(extract::State(state): State<A>) -> ApiResult
let stats = adaptor.get_stats().await.map_err(ApiError::AdaptorError)?; let stats = adaptor.get_stats().await.map_err(ApiError::AdaptorError)?;
Ok(Json(StatsResponse { Ok(Json(stats.into()))
event_count: stats.event_count,
person_count: stats.person_count,
version: env!("CARGO_PKG_VERSION").to_string(),
}))
} }

View file

@ -12,3 +12,6 @@ pub use get_people::get_people;
mod get_person; mod get_person;
pub use get_person::get_person; pub use get_person::get_person;
mod update_person;
pub use update_person::update_person;

View file

@ -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<A: Adaptor>(
extract::State(state): State<A>,
Path((event_id, person_name)): Path<(String, String)>,
Json(input): Json<UpdatePersonInput>,
) -> ApiResult<PersonResponse, A> {
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(),
))
}