use std::{collections::HashMap, sync::Arc}; use axum::{extract::{Path, State}, Json}; use reqwest::{StatusCode}; use sea_orm::{ActiveModelTrait, ActiveValue::{NotSet, Set}, ColumnTrait, EntityTrait, JoinType, QueryFilter, QuerySelect, RelationTrait, TryIntoModel}; use serde::{Deserialize, Serialize}; use utoipa::IntoParams; use crate::{entities::{book, book_instance, prelude::*}, routes::{auth::Claims, bal}, utils::auth::{user_is_bal_owner, user_is_book_instance_owner, user_is_owner_owner}, AppState}; #[derive(IntoParams)] #[into_params(names("id"), parameter_in = Path)] #[allow(dead_code)] struct BookInstanceByIdParams(u32); #[axum::debug_handler] #[utoipa::path( get, path = "/book_instance/{id}", params(BookInstanceByIdParams), security(("jwt" = [])), responses( (status = OK, body = book_instance::Model, description = "Found book instance with corresponding ID in the database"), (status = NOT_FOUND, description = "No book instance with this id exists in the database"), (status = FORBIDDEN, description = "You don't own the requested book instance"), ), summary = "Get a book instance by its ID", description = "Get a book instance from its ID", tag = "book-instance-api", )] pub async fn get_book_instance_by_id( State(state): State>, claims: Claims, Path(id): Path, ) -> (StatusCode, Json>) { if let Ok(Some(res)) = BookInstance::find_by_id(id).one(state.db_conn.as_ref()).await { if !user_is_book_instance_owner(claims.user_id, res.id, state.db_conn.as_ref()).await { return (StatusCode::FORBIDDEN, Json(None)); } (StatusCode::OK, Json(Some(res))) } else { (StatusCode::NOT_FOUND, Json(None)) } } #[derive(Deserialize, Serialize, utoipa::ToSchema)] pub struct BookInstanceCreateParams { book_id: u32, owner_id: u32, bal_id: u32, price: f32, } #[axum::debug_handler] #[utoipa::path( post, path = "/book_instance", request_body = BookInstanceCreateParams, security(("jwt" = [])), responses( (status = CREATED, body = book_instance::Model, description = "Successfully created book instance"), (status = FORBIDDEN, description = "You don't own the specified book instance"), ), summary = "Create a new book instance", description = "Create a new book instance", tag = "book-instance-api", )] pub async fn create_book_instance( State(state): State>, claims: Claims, Json(instance_payload): Json, ) -> (StatusCode, Json>) { if !user_is_bal_owner(claims.user_id, instance_payload.bal_id, state.db_conn.as_ref()).await { return (StatusCode::FORBIDDEN, Json(None)); } let book_instance = book_instance::ActiveModel { book_id: Set(instance_payload.book_id), owner_id: Set(instance_payload.owner_id), bal_id: Set(instance_payload.bal_id), price: Set(instance_payload.price), available: Set(true), id: NotSet, sold_price: NotSet, }; let b = book_instance.save(state.db_conn.as_ref()).await; match b { Err(e) => { log::error!(target: "api", "Error while inserting new book instance: {:#?}", e); (StatusCode::BAD_REQUEST, Json(None)) }, Ok(res) => (StatusCode::CREATED, Json(Some(res.try_into_model().expect("All fields should be set once the book instance is saved")))) } } #[derive(Deserialize, Serialize, utoipa::ToSchema)] pub struct BookInstanceUpdateParams { owner_id: Option, price: Option, } #[axum::debug_handler] #[utoipa::path( patch, path = "/book_instance/{id}", params(BookInstanceByIdParams), request_body = BookInstanceUpdateParams, security(("jwt" = [])), responses( (status = OK, body = book_instance::Model, description = "Successfully updated book instance"), (status = NOT_FOUND, description = "No book instance with specified id was found"), (status = FORBIDDEN, description = "You don't own the specified book instance"), ), summary = "Update a book instance", description = "Update a book instance", tag = "book-instance-api", )] pub async fn update_book_instance( State(state): State>, claims: Claims, Path(id): Path, Json(instance_payload): Json, ) -> (StatusCode, Json>) { if !user_is_book_instance_owner(claims.user_id, id, state.db_conn.as_ref()).await { return (StatusCode::FORBIDDEN, Json(None)); } if let Ok(Some(book_instance)) = BookInstance::find_by_id(id).one(state.db_conn.as_ref()).await { let mut book_instance: book_instance::ActiveModel = book_instance.into(); book_instance.price = match instance_payload.price { None => book_instance.price, Some(v) => Set(v) }; book_instance.owner_id = match instance_payload.owner_id { None => book_instance.owner_id, Some(v) => Set(v) }; match book_instance.update(state.db_conn.as_ref()).await { Err(e) => { log::error!(target: "api", "Error while updating book instance from api: {:#?}", e); (StatusCode::INTERNAL_SERVER_ERROR, Json(None)) }, Ok(res) => { let model = res.try_into_model().expect("All fields should be set once the book instance is saved"); (StatusCode::OK, Json(Some(model))) } } } else { (StatusCode::NOT_FOUND, Json(None)) } } #[derive(Deserialize, Serialize, utoipa::ToSchema)] pub struct BookInstanceSaleParams { price: f32, } #[axum::debug_handler] #[utoipa::path( post, path = "/book_instance/{id}/sell", params(BookInstanceByIdParams), request_body = BookInstanceSaleParams, security(("jwt" = [])), responses( (status = OK, body = book_instance::Model, description = "Successfully sold book instance"), (status = CONFLICT, description = "The specified book instance is not available"), (status = NOT_FOUND, description = "No book instance with specified id was found"), (status = FORBIDDEN, description = "You don't own the specified book instance"), ), summary = "Sell a book instance", description = "Sell a book instance", tag = "book-instance-api", )] pub async fn sell_book_instance( State(state): State>, claims: Claims, Path(id): Path, Json(instance_payload): Json, ) -> (StatusCode, Json>) { if !user_is_book_instance_owner(claims.user_id, id, state.db_conn.as_ref()).await { return (StatusCode::FORBIDDEN, Json(None)); } if let Ok(Some(book_instance)) = BookInstance::find_by_id(id).one(state.db_conn.as_ref()).await { if !book_instance.available { return (StatusCode::CONFLICT, Json(None)); } let mut book_instance: book_instance::ActiveModel = book_instance.into(); book_instance.sold_price = Set(Some(instance_payload.price)); book_instance.available = Set(false); match book_instance.update(state.db_conn.as_ref()).await { Err(e) => { log::error!(target: "api", "Error while selling book instance from api: {:#?}", e); (StatusCode::INTERNAL_SERVER_ERROR, Json(None)) }, Ok(res) => { let model = res.try_into_model().expect("All fields should be set once the book instance is saved"); (StatusCode::OK, Json(Some(model))) } } } else { (StatusCode::NOT_FOUND, Json(None)) } } #[axum::debug_handler] #[utoipa::path( post, path = "/book_instance/sell/bulk", request_body = HashMap, security(("jwt" = [])), responses( (status = OK, description = "Successfully sold book instances"), (status = CONFLICT, description = "One of the specified book instances is not available"), (status = NOT_FOUND, description = "One of the specified book instances was not found"), (status = FORBIDDEN, description = "You don't own one of the specified book instance"), ), summary = "Sell book instances in bulk", description = "Sell book instances in bulk. Payload: {book_instance_id: sold_price, ...}", tag = "book-instance-api", )] pub async fn bulk_sell_book_instance( State(state): State>, claims: Claims, Json(payload): Json>, ) -> StatusCode { // Here, we trust the client inputs valid book instances most of the time, so we already // allocate vector memory the size of the input map let mut book_instance_list: Vec<(book_instance::Model, f32)> = Vec::with_capacity(payload.len()); for instance in payload { if let Ok(Some(book_instance)) = BookInstance::find_by_id(instance.0).one(state.db_conn.as_ref()).await { if !user_is_bal_owner(claims.user_id, book_instance.bal_id, state.db_conn.as_ref()).await { return StatusCode::FORBIDDEN } else if !book_instance.available { return StatusCode::CONFLICT } else { book_instance_list.push((book_instance, instance.1)); } } else { return StatusCode::NOT_FOUND } } for instance in book_instance_list { let mut book_instance: book_instance::ActiveModel = instance.0.into(); book_instance.sold_price = Set(Some(instance.1)); book_instance.available = Set(false); let _ = book_instance.update(state.db_conn.as_ref()).await.unwrap(); } StatusCode::OK } #[axum::debug_handler] #[utoipa::path( post, path = "/book_instance/bulk", request_body = Vec, security(("jwt" = [])), responses( (status = CREATED, description = "Successfully created book instances"), (status = FORBIDDEN, description = "You don't own at least one specified bal of the book instances sent"), ), summary = "Create new book instances in bulk", description = "Create new book instances in bulk", tag = "book-instance-api", )] pub async fn bulk_create_book_instance( State(state): State>, claims: Claims, Json(instance_payload): Json>, ) -> StatusCode { for i in &instance_payload { if !user_is_bal_owner(claims.user_id, i.bal_id, state.db_conn.as_ref()).await { return StatusCode::FORBIDDEN; } } let instances = instance_payload .into_iter() .map(|p| { book_instance::ActiveModel { book_id: Set(p.book_id), owner_id: Set(p.owner_id), bal_id: Set(p.bal_id), price: Set(p.price), available: Set(true), id: NotSet, sold_price: NotSet } }); match BookInstance::insert_many(instances).exec(state.db_conn.as_ref()).await { Err(e) => { log::error!(target: "api", "Error while bulk inserting new book instances: {:#?}", e); StatusCode::INTERNAL_SERVER_ERROR }, Ok(_) => StatusCode::CREATED } } #[derive(IntoParams)] #[into_params(names("bal_id", "owner_id"), parameter_in = Path)] #[allow(dead_code)] struct BalOwnerByIdParams(u32, u32); #[axum::debug_handler] #[utoipa::path( get, path = "/bal/{bal_id}/owner/{owner_id}/book_instances", params(BalOwnerByIdParams), security(("jwt" = [])), responses( (status = OK, body = Vec, description = "Found book instances in the database"), (status = FORBIDDEN, description = "You do not own the specified owner"), ), summary = "Get books instances from an owner in a bal", description = "Lists all book instances an owner has in a bal. WARNING: If the bal or owner don't exist, the endpoint will return 200/OK with an empty list", tag = "book-instance-api", )] pub async fn get_bal_owner_book_instances( State(state): State>, claims: Claims, Path((bal_id, owner_id)): Path<(u32, u32)>, ) -> (StatusCode, Json>) { if !user_is_owner_owner(claims.user_id, owner_id, state.db_conn.as_ref()).await { return (StatusCode::FORBIDDEN, Json(vec![])); }// If a user owns an owner, it will own the bal the owner has books in, // so checking for bal ownership is unnecessary if let Ok(res) = BookInstance::find() .filter(book_instance::Column::BalId.eq(bal_id)) .filter(book_instance::Column::OwnerId.eq(owner_id)) .all(state.db_conn.as_ref()).await { (StatusCode::OK, Json(res)) } else { (StatusCode::INTERNAL_SERVER_ERROR, Json(vec![])) } } #[derive(IntoParams)] #[into_params(names("bal_id", "ean"), parameter_in = Path)] #[allow(dead_code)] struct BalBookByEanParams(u32, String); #[axum::debug_handler] #[utoipa::path( get, path = "/bal/{bal_id}/ean/{ean}/book_instances", params(BalBookByEanParams), security(("jwt" = [])), responses( (status = OK, body = Vec, description = "Found book instances in the database"), (status = FORBIDDEN, description = "You do not own the specified bal"), ), summary = "Get books instances with the specified ean in a bal", description = "Lists all book instances with the specified ean in a bal", tag = "book-instance-api", )] pub async fn get_bal_book_instances_by_ean( State(state): State>, claims: Claims, Path((bal_id, ean)): Path<(u32, String)>, ) -> (StatusCode, Json>) { if !user_is_bal_owner(claims.user_id, bal_id, state.db_conn.as_ref()).await { return (StatusCode::FORBIDDEN, Json(vec![])); } if let Ok(res) = BookInstance::find() .filter(book_instance::Column::BalId.eq(bal_id)) .join(JoinType::InnerJoin, book_instance::Relation::Book.def()) .filter(book::Column::Ean.eq(ean)) .all(state.db_conn.as_ref()).await { (StatusCode::OK, Json(res)) } else { (StatusCode::INTERNAL_SERVER_ERROR, Json(vec![])) } } #[derive(Deserialize, Serialize, utoipa::ToSchema)] pub struct BookInstanceSearchParams { title: Option, author: Option, } #[derive(Serialize, utoipa::ToSchema)] pub struct BookInstanceWithBook { book_instance: book_instance::Model, book: book::Model } #[axum::debug_handler] #[utoipa::path( post, path = "/bal/{id}/search", params(bal::BalByIdParams), request_body = BookInstanceSearchParams, security(("jwt" = [])), responses( (status = OK, body = Vec, description = "Found book instances in the database"), (status = FORBIDDEN, description = "You do not own the specified bal"), ), summary = "Search a BAL for books instances", description = "Lists all book instances that match the requested parameters in a bal", tag = "book-instance-api", )] pub async fn search_bal_book_instances( State(state): State>, claims: Claims, Path(bal_id): Path, Json(payload): Json, ) -> (StatusCode, Json>>) { if !user_is_bal_owner(claims.user_id, bal_id, state.db_conn.as_ref()).await { return (StatusCode::FORBIDDEN, Json(None)); } let mut search_query = BookInstance::find() .filter(book_instance::Column::BalId.eq(bal_id)) .filter(book_instance::Column::Available.eq(true)) .join(JoinType::InnerJoin, book_instance::Relation::Book.def()); if let Some(title) = payload.title { search_query = search_query.filter(book::Column::Title.like(format!("%{}%", title))) } if let Some(author) = payload.author { search_query = search_query.filter(book::Column::Author.like(format!("%{}%", author))) } if let Ok(res) = search_query.all(state.db_conn.as_ref()).await { let mut book_id_map = HashMap::new(); for instance in &res { if book_id_map.get(&instance.book_id).is_none() { book_id_map.insert(instance.book_id, Book::find_by_id(instance.book_id).one(state.db_conn.as_ref()).await.unwrap().unwrap()); } } return (StatusCode::OK, Json(Some(res.iter().map(|i| BookInstanceWithBook { book: book_id_map.get(&i.book_id).unwrap().clone(), book_instance: i.clone() }).collect()))) } else { (StatusCode::INTERNAL_SERVER_ERROR, Json(None)) } }