diff --git a/backend/Cargo.lock b/backend/Cargo.lock index 4e5bcab..9222e24 100644 --- a/backend/Cargo.lock +++ b/backend/Cargo.lock @@ -244,6 +244,7 @@ dependencies = [ "bitflags", "bytes", "futures-util", + "headers", "http", "http-body", "hyper", @@ -599,6 +600,7 @@ name = "crabfit_backend" version = "2.0.0" dependencies = [ "axum", + "base64 0.21.0", "bcrypt", "chrono", "common", @@ -1091,6 +1093,31 @@ dependencies = [ "hashbrown 0.12.3", ] +[[package]] +name = "headers" +version = "0.3.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f3e372db8e5c0d213e0cd0b9be18be2aca3d44cf2fe30a9d46a65581cd454584" +dependencies = [ + "base64 0.13.1", + "bitflags", + "bytes", + "headers-core", + "http", + "httpdate", + "mime", + "sha1", +] + +[[package]] +name = "headers-core" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7f66481bfee273957b1f20485a4ff3362987f85b2c236580d81b4eb7a326429" +dependencies = [ + "http", +] + [[package]] name = "heck" version = "0.3.3" diff --git a/backend/Cargo.toml b/backend/Cargo.toml index 63c90b9..c943b8b 100644 --- a/backend/Cargo.toml +++ b/backend/Cargo.toml @@ -9,7 +9,7 @@ edition = "2021" members = ["common", "adaptors/*"] [dependencies] -axum = "0.6.18" +axum = { version = "0.6.18", features = ["headers"] } serde = { version = "1.0.162", features = ["derive"] } tokio = { version = "1.28.0", features = ["macros", "rt-multi-thread"] } common = { path = "common" } @@ -28,3 +28,4 @@ tower_governor = "0.0.4" tower = "0.4.13" utoipa = { version = "3.3.0", features = ["axum_extras", "preserve_order"] } utoipa-swagger-ui = { version = "3.1.3", features = ["axum"] } +base64 = "0.21.0" diff --git a/backend/src/docs.rs b/backend/src/docs.rs new file mode 100644 index 0000000..372ce2a --- /dev/null +++ b/backend/src/docs.rs @@ -0,0 +1,52 @@ +use crate::payloads; +use crate::routes; + +use utoipa::{ + openapi::security::{HttpAuthScheme, HttpBuilder, SecurityScheme}, + Modify, OpenApi, +}; + +// OpenAPI documentation +#[derive(OpenApi)] +#[openapi( + info(title = "Crab Fit API"), + paths( + routes::get_stats::get_stats, + routes::create_event::create_event, + routes::get_event::get_event, + routes::get_people::get_people, + routes::get_person::get_person, + routes::update_person::update_person, + ), + components(schemas( + payloads::StatsResponse, + payloads::EventResponse, + payloads::PersonResponse, + payloads::EventInput, + payloads::PersonInput, + )), + tags( + (name = "info"), + (name = "event"), + (name = "person"), + ), + modifiers(&SecurityAddon), +)] +pub struct ApiDoc; + +struct SecurityAddon; + +// Add password auth spec +impl Modify for SecurityAddon { + fn modify(&self, openapi: &mut utoipa::openapi::OpenApi) { + openapi.components.as_mut().unwrap().add_security_scheme( + "password", + SecurityScheme::Http( + HttpBuilder::new() + .scheme(HttpAuthScheme::Bearer) + .bearer_format("base64") + .build(), + ), + ); + } +} diff --git a/backend/src/main.rs b/backend/src/main.rs index 8d7b5f1..30d9194 100644 --- a/backend/src/main.rs +++ b/backend/src/main.rs @@ -16,6 +16,9 @@ use tower_http::{cors::CorsLayer, trace::TraceLayer}; use utoipa::OpenApi; use utoipa_swagger_ui::SwaggerUi; +use crate::docs::ApiDoc; + +mod docs; mod errors; mod payloads; mod routes; @@ -33,35 +36,6 @@ async fn main() { // Load env dotenv::dotenv().ok(); - #[derive(OpenApi)] - #[openapi( - info(title = "Crab Fit API"), - paths( - routes::get_stats::get_stats, - routes::create_event::create_event, - routes::get_event::get_event, - routes::get_people::get_people, - routes::get_person::get_person, - routes::update_person::update_person, - ), - components( - schemas( - payloads::StatsResponse, - payloads::EventResponse, - payloads::PersonResponse, - payloads::EventInput, - payloads::GetPersonInput, - payloads::UpdatePersonInput, - ), - ), - tags( - (name = "info"), - (name = "event"), - (name = "person"), - ), - )] - struct ApiDoc; - let shared_state = Arc::new(Mutex::new(ApiState { adaptor: SqlAdaptor::new().await, })); diff --git a/backend/src/payloads.rs b/backend/src/payloads.rs index 60779e6..8ed6df2 100644 --- a/backend/src/payloads.rs +++ b/backend/src/payloads.rs @@ -70,12 +70,6 @@ impl From for PersonResponse { } #[derive(Deserialize, ToSchema)] -pub struct GetPersonInput { - pub password: Option, -} - -#[derive(Deserialize, ToSchema)] -pub struct UpdatePersonInput { - pub password: Option, +pub struct PersonInput { pub availability: Vec, } diff --git a/backend/src/routes/get_person.rs b/backend/src/routes/get_person.rs index 90c104f..2f127e9 100644 --- a/backend/src/routes/get_person.rs +++ b/backend/src/routes/get_person.rs @@ -1,12 +1,14 @@ use axum::{ extract::{self, Path}, - Json, + headers::{authorization::Bearer, Authorization}, + Json, TypedHeader, }; +use base64::{engine::general_purpose, Engine}; use common::{adaptor::Adaptor, person::Person}; use crate::{ errors::ApiError, - payloads::{ApiResult, GetPersonInput, PersonResponse}, + payloads::{ApiResult, PersonResponse}, State, }; @@ -17,7 +19,7 @@ use crate::{ ("event_id", description = "The ID of the event"), ("person_name", description = "The name of the person"), ), - request_body(content = GetPersonInput, description = "Person details"), + security((), ("password" = [])), responses( (status = 200, description = "Ok", body = PersonResponse), (status = 401, description = "Incorrect password"), @@ -32,15 +34,12 @@ use crate::{ pub async fn get_person( extract::State(state): State, Path((event_id, person_name)): Path<(String, String)>, - input: Option>, + bearer: Option>>, ) -> ApiResult { let adaptor = &state.lock().await.adaptor; // Get inputted password - let password = match input { - Some(Json(i)) => i.password, - None => None, - }; + let password = parse_password(bearer); let existing_people = adaptor .get_people(event_id.clone()) @@ -96,9 +95,20 @@ pub async fn get_person( } } +pub fn parse_password(bearer: Option>>) -> Option { + bearer.map(|TypedHeader(Authorization(b))| { + String::from_utf8( + general_purpose::STANDARD + .decode(b.token().trim()) + .unwrap_or(vec![]), + ) + .unwrap_or("".to_owned()) + }) +} + 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), + Some(hash) => bcrypt::verify(raw.unwrap_or("".to_owned()), 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/update_person.rs b/backend/src/routes/update_person.rs index 521f71b..8bb45e3 100644 --- a/backend/src/routes/update_person.rs +++ b/backend/src/routes/update_person.rs @@ -1,16 +1,17 @@ use axum::{ extract::{self, Path}, - Json, + headers::{authorization::Bearer, Authorization}, + Json, TypedHeader, }; use common::{adaptor::Adaptor, person::Person}; use crate::{ errors::ApiError, - payloads::{ApiResult, PersonResponse, UpdatePersonInput}, + payloads::{ApiResult, PersonInput, PersonResponse}, State, }; -use super::get_person::verify_password; +use super::get_person::{parse_password, verify_password}; #[utoipa::path( patch, @@ -19,7 +20,8 @@ use super::get_person::verify_password; ("event_id", description = "The ID of the event"), ("person_name", description = "The name of the person"), ), - request_body(content = UpdatePersonInput, description = "Person details"), + security((), ("password" = [])), + request_body(content = PersonInput, description = "Person details"), responses( (status = 200, description = "Ok", body = PersonResponse), (status = 401, description = "Incorrect password"), @@ -34,7 +36,8 @@ 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, + bearer: Option>>, + Json(input): Json, ) -> ApiResult { let adaptor = &state.lock().await.adaptor; @@ -56,7 +59,7 @@ pub async fn update_person( .ok_or(ApiError::NotFound)?; // Verify password (if set) - if !verify_password(&existing_person, input.password) { + if !verify_password(&existing_person, parse_password(bearer)) { return Err(ApiError::NotAuthorized); }