Co-locate related routes
This commit is contained in:
parent
aa3b323cb6
commit
bf5bcf9992
|
|
@ -11,12 +11,12 @@ use utoipa::{
|
||||||
#[openapi(
|
#[openapi(
|
||||||
info(title = "Crab Fit API"),
|
info(title = "Crab Fit API"),
|
||||||
paths(
|
paths(
|
||||||
routes::get_stats::get_stats,
|
routes::stats::get_stats,
|
||||||
routes::create_event::create_event,
|
routes::event::create_event,
|
||||||
routes::get_event::get_event,
|
routes::event::get_event,
|
||||||
routes::get_people::get_people,
|
routes::person::get_people,
|
||||||
routes::get_person::get_person,
|
routes::person::get_person,
|
||||||
routes::update_person::update_person,
|
routes::person::update_person,
|
||||||
),
|
),
|
||||||
components(schemas(
|
components(schemas(
|
||||||
payloads::StatsResponse,
|
payloads::StatsResponse,
|
||||||
|
|
|
||||||
|
|
@ -69,12 +69,18 @@ async fn main() {
|
||||||
let app = Router::new()
|
let app = Router::new()
|
||||||
.merge(SwaggerUi::new("/docs").url("/docs/openapi.json", ApiDoc::openapi()))
|
.merge(SwaggerUi::new("/docs").url("/docs/openapi.json", ApiDoc::openapi()))
|
||||||
.route("/", get(get_root))
|
.route("/", get(get_root))
|
||||||
.route("/stats", get(get_stats))
|
.route("/stats", get(stats::get_stats))
|
||||||
.route("/event", post(create_event))
|
.route("/event", post(event::create_event))
|
||||||
.route("/event/:event_id", get(get_event))
|
.route("/event/:event_id", get(event::get_event))
|
||||||
.route("/event/:event_id/people", get(get_people))
|
.route("/event/:event_id/people", get(person::get_people))
|
||||||
.route("/event/:event_id/people/:person_name", get(get_person))
|
.route(
|
||||||
.route("/event/:event_id/people/:person_name", patch(update_person))
|
"/event/:event_id/people/:person_name",
|
||||||
|
get(person::get_person),
|
||||||
|
)
|
||||||
|
.route(
|
||||||
|
"/event/:event_id/people/:person_name",
|
||||||
|
patch(person::update_person),
|
||||||
|
)
|
||||||
.with_state(shared_state)
|
.with_state(shared_state)
|
||||||
.layer(cors)
|
.layer(cors)
|
||||||
.layer(rate_limit)
|
.layer(rate_limit)
|
||||||
|
|
|
||||||
|
|
@ -1,14 +1,49 @@
|
||||||
use axum::{extract, http::StatusCode, Json};
|
use axum::{
|
||||||
|
extract::{self, Path},
|
||||||
|
http::StatusCode,
|
||||||
|
Json,
|
||||||
|
};
|
||||||
use common::{adaptor::Adaptor, event::Event};
|
use common::{adaptor::Adaptor, event::Event};
|
||||||
use rand::{seq::SliceRandom, thread_rng, Rng};
|
use rand::{seq::SliceRandom, thread_rng, Rng};
|
||||||
use regex::Regex;
|
use regex::Regex;
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
errors::ApiError,
|
errors::ApiError,
|
||||||
payloads::{EventInput, EventResponse},
|
payloads::{ApiResult, EventInput, EventResponse},
|
||||||
State,
|
State,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
#[utoipa::path(
|
||||||
|
get,
|
||||||
|
path = "/event/{event_id}",
|
||||||
|
params(
|
||||||
|
("event_id", description = "The ID of the event"),
|
||||||
|
),
|
||||||
|
responses(
|
||||||
|
(status = 200, description = "Ok", body = EventResponse),
|
||||||
|
(status = 404, description = "Not found"),
|
||||||
|
(status = 429, description = "Too many requests"),
|
||||||
|
),
|
||||||
|
tag = "event",
|
||||||
|
)]
|
||||||
|
/// Get details about an event
|
||||||
|
pub async fn get_event<A: Adaptor>(
|
||||||
|
extract::State(state): State<A>,
|
||||||
|
Path(event_id): Path<String>,
|
||||||
|
) -> ApiResult<EventResponse, A> {
|
||||||
|
let adaptor = &state.lock().await.adaptor;
|
||||||
|
|
||||||
|
let event = adaptor
|
||||||
|
.get_event(event_id)
|
||||||
|
.await
|
||||||
|
.map_err(ApiError::AdaptorError)?;
|
||||||
|
|
||||||
|
match event {
|
||||||
|
Some(event) => Ok(Json(event.into())),
|
||||||
|
None => Err(ApiError::NotFound),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[utoipa::path(
|
#[utoipa::path(
|
||||||
post,
|
post,
|
||||||
path = "/event",
|
path = "/event",
|
||||||
|
|
@ -1,42 +0,0 @@
|
||||||
use axum::{
|
|
||||||
extract::{self, Path},
|
|
||||||
Json,
|
|
||||||
};
|
|
||||||
use common::adaptor::Adaptor;
|
|
||||||
|
|
||||||
use crate::{
|
|
||||||
errors::ApiError,
|
|
||||||
payloads::{ApiResult, EventResponse},
|
|
||||||
State,
|
|
||||||
};
|
|
||||||
|
|
||||||
#[utoipa::path(
|
|
||||||
get,
|
|
||||||
path = "/event/{event_id}",
|
|
||||||
params(
|
|
||||||
("event_id", description = "The ID of the event"),
|
|
||||||
),
|
|
||||||
responses(
|
|
||||||
(status = 200, description = "Ok", body = EventResponse),
|
|
||||||
(status = 404, description = "Not found"),
|
|
||||||
(status = 429, description = "Too many requests"),
|
|
||||||
),
|
|
||||||
tag = "event",
|
|
||||||
)]
|
|
||||||
/// Get details about an event
|
|
||||||
pub async fn get_event<A: Adaptor>(
|
|
||||||
extract::State(state): State<A>,
|
|
||||||
Path(event_id): Path<String>,
|
|
||||||
) -> ApiResult<EventResponse, A> {
|
|
||||||
let adaptor = &state.lock().await.adaptor;
|
|
||||||
|
|
||||||
let event = adaptor
|
|
||||||
.get_event(event_id)
|
|
||||||
.await
|
|
||||||
.map_err(ApiError::AdaptorError)?;
|
|
||||||
|
|
||||||
match event {
|
|
||||||
Some(event) => Ok(Json(event.into())),
|
|
||||||
None => Err(ApiError::NotFound),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,42 +0,0 @@
|
||||||
use axum::{
|
|
||||||
extract::{self, Path},
|
|
||||||
Json,
|
|
||||||
};
|
|
||||||
use common::adaptor::Adaptor;
|
|
||||||
|
|
||||||
use crate::{
|
|
||||||
errors::ApiError,
|
|
||||||
payloads::{ApiResult, PersonResponse},
|
|
||||||
State,
|
|
||||||
};
|
|
||||||
|
|
||||||
#[utoipa::path(
|
|
||||||
get,
|
|
||||||
path = "/event/{event_id}/people",
|
|
||||||
params(
|
|
||||||
("event_id", description = "The ID of the event"),
|
|
||||||
),
|
|
||||||
responses(
|
|
||||||
(status = 200, description = "Ok", body = [PersonResponse]),
|
|
||||||
(status = 404, description = "Event not found"),
|
|
||||||
(status = 429, description = "Too many requests"),
|
|
||||||
),
|
|
||||||
tag = "person",
|
|
||||||
)]
|
|
||||||
/// Get availabilities for an event
|
|
||||||
pub async fn get_people<A: Adaptor>(
|
|
||||||
extract::State(state): State<A>,
|
|
||||||
Path(event_id): Path<String>,
|
|
||||||
) -> ApiResult<Vec<PersonResponse>, A> {
|
|
||||||
let adaptor = &state.lock().await.adaptor;
|
|
||||||
|
|
||||||
let people = adaptor
|
|
||||||
.get_people(event_id)
|
|
||||||
.await
|
|
||||||
.map_err(ApiError::AdaptorError)?;
|
|
||||||
|
|
||||||
match people {
|
|
||||||
Some(people) => Ok(Json(people.into_iter().map(|p| p.into()).collect())),
|
|
||||||
None => Err(ApiError::NotFound),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,17 +1,3 @@
|
||||||
pub mod get_event;
|
pub mod event;
|
||||||
pub use get_event::get_event;
|
pub mod person;
|
||||||
|
pub mod stats;
|
||||||
pub mod get_stats;
|
|
||||||
pub use get_stats::get_stats;
|
|
||||||
|
|
||||||
pub mod create_event;
|
|
||||||
pub use create_event::create_event;
|
|
||||||
|
|
||||||
pub mod get_people;
|
|
||||||
pub use get_people::get_people;
|
|
||||||
|
|
||||||
pub mod get_person;
|
|
||||||
pub use get_person::get_person;
|
|
||||||
|
|
||||||
pub mod update_person;
|
|
||||||
pub use update_person::update_person;
|
|
||||||
|
|
|
||||||
|
|
@ -8,10 +8,41 @@ use common::{adaptor::Adaptor, person::Person};
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
errors::ApiError,
|
errors::ApiError,
|
||||||
payloads::{ApiResult, PersonResponse},
|
payloads::{ApiResult, PersonInput, PersonResponse},
|
||||||
State,
|
State,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
#[utoipa::path(
|
||||||
|
get,
|
||||||
|
path = "/event/{event_id}/people",
|
||||||
|
params(
|
||||||
|
("event_id", description = "The ID of the event"),
|
||||||
|
),
|
||||||
|
responses(
|
||||||
|
(status = 200, description = "Ok", body = [PersonResponse]),
|
||||||
|
(status = 404, description = "Event not found"),
|
||||||
|
(status = 429, description = "Too many requests"),
|
||||||
|
),
|
||||||
|
tag = "person",
|
||||||
|
)]
|
||||||
|
/// Get availabilities for an event
|
||||||
|
pub async fn get_people<A: Adaptor>(
|
||||||
|
extract::State(state): State<A>,
|
||||||
|
Path(event_id): Path<String>,
|
||||||
|
) -> ApiResult<Vec<PersonResponse>, A> {
|
||||||
|
let adaptor = &state.lock().await.adaptor;
|
||||||
|
|
||||||
|
let people = adaptor
|
||||||
|
.get_people(event_id)
|
||||||
|
.await
|
||||||
|
.map_err(ApiError::AdaptorError)?;
|
||||||
|
|
||||||
|
match people {
|
||||||
|
Some(people) => Ok(Json(people.into_iter().map(|p| p.into()).collect())),
|
||||||
|
None => Err(ApiError::NotFound),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[utoipa::path(
|
#[utoipa::path(
|
||||||
get,
|
get,
|
||||||
path = "/event/{event_id}/people/{person_name}",
|
path = "/event/{event_id}/people/{person_name}",
|
||||||
|
|
@ -95,6 +126,73 @@ pub async fn get_person<A: Adaptor>(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[utoipa::path(
|
||||||
|
patch,
|
||||||
|
path = "/event/{event_id}/people/{person_name}",
|
||||||
|
params(
|
||||||
|
("event_id", description = "The ID of the event"),
|
||||||
|
("person_name", description = "The name of the person"),
|
||||||
|
),
|
||||||
|
security((), ("password" = [])),
|
||||||
|
request_body(content = PersonInput, description = "Person details"),
|
||||||
|
responses(
|
||||||
|
(status = 200, description = "Ok", body = PersonResponse),
|
||||||
|
(status = 401, description = "Incorrect password"),
|
||||||
|
(status = 404, description = "Event or person not found"),
|
||||||
|
(status = 415, description = "Unsupported input format"),
|
||||||
|
(status = 422, description = "Invalid input provided"),
|
||||||
|
(status = 429, description = "Too many requests"),
|
||||||
|
),
|
||||||
|
tag = "person",
|
||||||
|
)]
|
||||||
|
/// Update a person's availabilities
|
||||||
|
pub async fn update_person<A: Adaptor>(
|
||||||
|
extract::State(state): State<A>,
|
||||||
|
Path((event_id, person_name)): Path<(String, String)>,
|
||||||
|
bearer: Option<TypedHeader<Authorization<Bearer>>>,
|
||||||
|
Json(input): Json<PersonInput>,
|
||||||
|
) -> 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, parse_password(bearer)) {
|
||||||
|
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(),
|
||||||
|
))
|
||||||
|
}
|
||||||
|
|
||||||
pub fn parse_password(bearer: Option<TypedHeader<Authorization<Bearer>>>) -> Option<String> {
|
pub fn parse_password(bearer: Option<TypedHeader<Authorization<Bearer>>>) -> Option<String> {
|
||||||
bearer.map(|TypedHeader(Authorization(b))| {
|
bearer.map(|TypedHeader(Authorization(b))| {
|
||||||
String::from_utf8(
|
String::from_utf8(
|
||||||
|
|
@ -1,81 +0,0 @@
|
||||||
use axum::{
|
|
||||||
extract::{self, Path},
|
|
||||||
headers::{authorization::Bearer, Authorization},
|
|
||||||
Json, TypedHeader,
|
|
||||||
};
|
|
||||||
use common::{adaptor::Adaptor, person::Person};
|
|
||||||
|
|
||||||
use crate::{
|
|
||||||
errors::ApiError,
|
|
||||||
payloads::{ApiResult, PersonInput, PersonResponse},
|
|
||||||
State,
|
|
||||||
};
|
|
||||||
|
|
||||||
use super::get_person::{parse_password, verify_password};
|
|
||||||
|
|
||||||
#[utoipa::path(
|
|
||||||
patch,
|
|
||||||
path = "/event/{event_id}/people/{person_name}",
|
|
||||||
params(
|
|
||||||
("event_id", description = "The ID of the event"),
|
|
||||||
("person_name", description = "The name of the person"),
|
|
||||||
),
|
|
||||||
security((), ("password" = [])),
|
|
||||||
request_body(content = PersonInput, description = "Person details"),
|
|
||||||
responses(
|
|
||||||
(status = 200, description = "Ok", body = PersonResponse),
|
|
||||||
(status = 401, description = "Incorrect password"),
|
|
||||||
(status = 404, description = "Event or person not found"),
|
|
||||||
(status = 415, description = "Unsupported input format"),
|
|
||||||
(status = 422, description = "Invalid input provided"),
|
|
||||||
(status = 429, description = "Too many requests"),
|
|
||||||
),
|
|
||||||
tag = "person",
|
|
||||||
)]
|
|
||||||
/// Update a person's availabilities
|
|
||||||
pub async fn update_person<A: Adaptor>(
|
|
||||||
extract::State(state): State<A>,
|
|
||||||
Path((event_id, person_name)): Path<(String, String)>,
|
|
||||||
bearer: Option<TypedHeader<Authorization<Bearer>>>,
|
|
||||||
Json(input): Json<PersonInput>,
|
|
||||||
) -> 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, parse_password(bearer)) {
|
|
||||||
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(),
|
|
||||||
))
|
|
||||||
}
|
|
||||||
Loading…
Reference in a new issue