Compare commits
	
		
			No commits in common. "3320d1400ce83f7d9c507af6f72720792a5bc728" and "c36a38cd7a7c0226fd97bdb1b9f004efbdbdbb01" have entirely different histories.
		
	
	
		
			3320d1400c
			...
			c36a38cd7a
		
	
		
					 9 changed files with 14 additions and 175 deletions
				
			
		
							
								
								
									
										11
									
								
								Cargo.lock
									
										
									
										generated
									
									
									
								
							
							
						
						
									
										11
									
								
								Cargo.lock
									
										
									
										generated
									
									
									
								
							|  | @ -53,7 +53,6 @@ dependencies = [ | |||
|  "openssl", | ||||
|  "password-hash", | ||||
|  "pretty_env_logger", | ||||
|  "quick-xml", | ||||
|  "rand_core 0.9.3", | ||||
|  "reqwest", | ||||
|  "sea-orm", | ||||
|  | @ -2083,16 +2082,6 @@ dependencies = [ | |||
|  "syn 1.0.109", | ||||
| ] | ||||
| 
 | ||||
| [[package]] | ||||
| name = "quick-xml" | ||||
| version = "0.38.1" | ||||
| source = "registry+https://github.com/rust-lang/crates.io-index" | ||||
| checksum = "9845d9dccf565065824e69f9f235fafba1587031eda353c1f1561cd6a6be78f4" | ||||
| dependencies = [ | ||||
|  "memchr", | ||||
|  "serde", | ||||
| ] | ||||
| 
 | ||||
| [[package]] | ||||
| name = "quinn" | ||||
| version = "0.11.8" | ||||
|  |  | |||
|  | @ -25,5 +25,4 @@ rand_core = { version = "0.9.3", features = ["os_rng"] } | |||
| password-hash = { version = "0.5.0", features = ["getrandom"] } | ||||
| jsonwebtoken = "9.3.1" | ||||
| openssl = { version = "0.10.73", features = ["vendored"] } | ||||
| quick-xml = { version = "0.38.1", features = ["serialize"] } | ||||
| 
 | ||||
|  |  | |||
|  | @ -170,7 +170,6 @@ async fn run_server(db: Arc<DatabaseConnection>) { | |||
|         .routes(routes!(routes::book_instance::update_book_instance)) | ||||
|         .routes(routes!(routes::book_instance::sell_book_instance)) | ||||
|         .routes(routes!(routes::book_instance::bulk_create_book_instance)) | ||||
|         .routes(routes!(routes::book_instance::get_bal_owner_book_instances)) | ||||
|         // Owner API
 | ||||
|         .routes(routes!(routes::owner::get_owner_by_id)) | ||||
|         .routes(routes!(routes::owner::create_owner)) | ||||
|  |  | |||
|  | @ -57,7 +57,7 @@ pub struct BalCreateParams { | |||
|     request_body = BalCreateParams, | ||||
|     security(("jwt" = [])), | ||||
|     responses( | ||||
|         (status = CREATED, body = bal::Model, description = "Successfully created BAL"), | ||||
|         (status = OK, body = bal::Model, description = "Successfully created BAL"), | ||||
|     ), | ||||
|     summary = "Create a new bal", | ||||
|     description = "Create a new bal", | ||||
|  | @ -80,7 +80,7 @@ pub async fn create_bal( | |||
|             log::error!(target: "api", "Error while inserting new bal: {:#?}", e); | ||||
|             (StatusCode::BAD_REQUEST, Json(None)) | ||||
|         }, | ||||
|         Ok(res) => (StatusCode::CREATED, Json(Some(res.try_into_model().expect("All fields should be set once the bal is saved")))) | ||||
|         Ok(res) => (StatusCode::OK, Json(Some(res.try_into_model().expect("All fields should be set once the bal is saved")))) | ||||
|     } | ||||
| } | ||||
| 
 | ||||
|  |  | |||
|  | @ -6,7 +6,7 @@ use sea_orm::{ActiveModelTrait, ActiveValue::{NotSet, Set}, ColumnTrait, EntityT | |||
| use serde::{Deserialize, Serialize}; | ||||
| use utoipa::IntoParams; | ||||
| 
 | ||||
| use crate::{entities::{book, prelude::Book}, utils::{bnf}}; | ||||
| use crate::{entities::{book, prelude::{Book}}, utils::open_library}; | ||||
| 
 | ||||
| use crate::AppState; | ||||
| 
 | ||||
|  | @ -72,7 +72,7 @@ pub async fn get_book_by_ean( | |||
|     if let Ok(Some(res)) = Book::find().filter(book::Column::Ean.eq(&ean)).one(state.db_conn.as_ref()).await { | ||||
|         (StatusCode::OK, Json(Some(res))) | ||||
|     } else { | ||||
|         let fetched_book = bnf::fetch_book_by_ean(&state.web_client, &ean).await; | ||||
|         let fetched_book = open_library::fetch_book_by_ean(&state.web_client, &ean).await; | ||||
|         if let Some(book) = fetched_book { | ||||
|             let res = Book::insert(book.to_active_model()).exec(state.db_conn.as_ref()).await.unwrap(); | ||||
|             (StatusCode::OK, Json(Some(book::Model { | ||||
|  | @ -101,7 +101,7 @@ pub struct BookCreateParams { | |||
|     request_body = BookCreateParams, | ||||
|     security(("jwt" = [])), | ||||
|     responses( | ||||
|         (status = CREATED, body = book::Model, description = "Successfully saved book data"), | ||||
|         (status = OK, body = book::Model, description = "Successfully saved book data"), | ||||
|         (status = CONFLICT, body = book::Model, description = "A book with the same EAN already exists. Replies with the data of the already saved book."), | ||||
|     ), | ||||
|     summary = "Manually add book data to the database", | ||||
|  | @ -129,6 +129,6 @@ pub async fn create_book( | |||
|             log::error!(target: "api", "Error while inserting new book: {:#?}", 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 is saved")))) | ||||
|         Ok(res) => (StatusCode::OK, Json(Some(res.try_into_model().expect("All fields should be set once the book is saved")))) | ||||
|     } | ||||
| } | ||||
|  |  | |||
|  | @ -2,11 +2,11 @@ use std::sync::Arc; | |||
| 
 | ||||
| use axum::{extract::{Path, State}, Json}; | ||||
| use reqwest::{StatusCode}; | ||||
| use sea_orm::{ActiveModelTrait, ActiveValue::{NotSet, Set}, ColumnTrait, EntityTrait, QueryFilter, TryIntoModel}; | ||||
| use sea_orm::{ActiveModelTrait, ActiveValue::{NotSet, Set}, EntityTrait, TryIntoModel}; | ||||
| use serde::{Deserialize, Serialize}; | ||||
| use utoipa::IntoParams; | ||||
| 
 | ||||
| use crate::{entities::{book_instance, prelude::*}, routes::auth::Claims, utils::auth::{user_is_bal_owner, user_is_book_instance_owner, user_is_owner_owner}, AppState}; | ||||
| use crate::{entities::{book_instance, prelude::*}, routes::auth::Claims, utils::auth::{user_is_bal_owner, user_is_book_instance_owner}, AppState}; | ||||
| 
 | ||||
| 
 | ||||
| #[derive(IntoParams)] | ||||
|  | @ -59,7 +59,7 @@ pub struct BookInstanceCreateParams { | |||
|     request_body = BookInstanceCreateParams, | ||||
|     security(("jwt" = [])), | ||||
|     responses( | ||||
|         (status = CREATED, body = book_instance::Model, description = "Successfully created book instance"), | ||||
|         (status = OK, 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", | ||||
|  | @ -91,7 +91,7 @@ pub async fn create_book_instance( | |||
|             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")))) | ||||
|         Ok(res) => (StatusCode::OK, Json(Some(res.try_into_model().expect("All fields should be set once the book instance is saved")))) | ||||
|     } | ||||
| } | ||||
| 
 | ||||
|  | @ -216,7 +216,7 @@ pub async fn sell_book_instance( | |||
|     request_body = Vec<BookInstanceCreateParams>, | ||||
|     security(("jwt" = [])), | ||||
|     responses( | ||||
|         (status = CREATED, description = "Successfully created book instances"), | ||||
|         (status = OK, 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", | ||||
|  | @ -253,45 +253,6 @@ pub async fn bulk_create_book_instance( | |||
|             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<book_instance::Model>, 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<Arc<AppState>>, | ||||
|     claims: Claims, | ||||
|     Path((bal_id, owner_id)): Path<(u32, u32)>, | ||||
| ) -> (StatusCode, Json<Vec<book_instance::Model>>) { | ||||
|     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![])) | ||||
|         Ok(_) => StatusCode::OK | ||||
|     } | ||||
| } | ||||
|  |  | |||
|  | @ -59,7 +59,7 @@ pub struct OwnerCreateParams { | |||
|     request_body = OwnerCreateParams, | ||||
|     security(("jwt" = [])), | ||||
|     responses( | ||||
|         (status = CREATED, body = owner::Model, description = "Successfully created owner"), | ||||
|         (status = OK, body = owner::Model, description = "Successfully created owner"), | ||||
|     ), | ||||
|     summary = "Create a new owner", | ||||
|     description = "Create a new owner", | ||||
|  | @ -88,7 +88,7 @@ pub async fn create_owner( | |||
|         Ok(res) => { | ||||
|             let model = res.try_into_model().expect("All fields should be set once the owner is saved"); | ||||
|             let _ = state.event_bus.send(Event::WebsocketBroadcast(WebsocketMessage::NewOwner(Arc::new(model.clone())))); | ||||
|             (StatusCode::CREATED, Json(Some(model))) | ||||
|             (StatusCode::OK, Json(Some(model))) | ||||
|         } | ||||
|     } | ||||
| } | ||||
|  |  | |||
							
								
								
									
										108
									
								
								src/utils/bnf.rs
									
										
									
									
									
								
							
							
						
						
									
										108
									
								
								src/utils/bnf.rs
									
										
									
									
									
								
							|  | @ -1,108 +0,0 @@ | |||
| use reqwest::StatusCode; | ||||
| use sea_orm::ActiveValue::{NotSet, Set}; | ||||
| use serde::Deserialize; | ||||
| use serde_json::Value; | ||||
| 
 | ||||
| use crate::entities::book; | ||||
| 
 | ||||
| #[derive(Deserialize)] | ||||
| pub struct Response { | ||||
|     #[serde(rename = "numberOfRecords")] | ||||
|     records_number: u32, | ||||
|     #[serde(rename = "records")] | ||||
|     records: Vec<RecordListElement> | ||||
| } | ||||
| 
 | ||||
| #[derive(Deserialize)] | ||||
| pub struct RecordListElement { | ||||
|     #[serde(rename = "record")] | ||||
|     record: Record | ||||
| } | ||||
| 
 | ||||
| #[derive(Deserialize)] | ||||
| pub struct Record { | ||||
|     #[serde(rename = "recordData")] | ||||
|     record_data: RecordData | ||||
| } | ||||
| 
 | ||||
| #[derive(Deserialize)] | ||||
| pub struct RecordData { | ||||
|     #[serde(rename = "record")] | ||||
|     record: RecordDataFields | ||||
| } | ||||
| 
 | ||||
| #[derive(Deserialize)] | ||||
| pub struct RecordDataFields { | ||||
|     #[serde(rename = "datafield")] | ||||
|     datafields: Vec<DataField> | ||||
| } | ||||
| 
 | ||||
| #[derive(Deserialize)] | ||||
| pub struct DataField { | ||||
|     #[serde(rename = "@tag")] | ||||
|     tag: String, | ||||
|     #[serde(rename = "subfield")] | ||||
|     subfields: Vec<SubField> | ||||
| } | ||||
| 
 | ||||
| #[derive(Deserialize, Clone)] | ||||
| pub struct SubField { | ||||
|     #[serde(rename = "@code")] | ||||
|     code: String, | ||||
|     #[serde(rename = "$text")] | ||||
|     value: String | ||||
| } | ||||
| 
 | ||||
| 
 | ||||
| pub struct FetchedBook { | ||||
|     pub ean: String, | ||||
|     pub title: String, | ||||
|     pub author: String | ||||
| } | ||||
| 
 | ||||
| impl FetchedBook { | ||||
|     pub fn to_active_model(&self) -> book::ActiveModel { | ||||
|         book::ActiveModel { | ||||
|             id: NotSet, | ||||
|             ean: Set(self.ean.clone()), | ||||
|             title: Set(self.title.clone()), | ||||
|             author: Set(self.author.clone()) | ||||
|         } | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| pub async fn fetch_book_by_ean(web_client: &reqwest::Client, ean: &String) -> Option<FetchedBook> { | ||||
|     let body = web_client.execute( | ||||
|         web_client.get(format!("https://catalogue.bnf.fr/api/SRU?version=1.2&operation=searchRetrieve&query=bib.isbn any \"{ean}\"")) | ||||
|             .build() | ||||
|             .expect("get request creation failed") | ||||
|     ).await.unwrap(); | ||||
|     match body.status() { | ||||
|         StatusCode::OK => { | ||||
|             let res = body.text().await.unwrap().replace("\n", ""); | ||||
|             log::debug!(target: "api", "BNF book fetch result: {res:#?}"); | ||||
|             let der: Result<Response, quick_xml::DeError> = quick_xml::de::from_str(&res); | ||||
|             match der { | ||||
|                 Ok(v) => { | ||||
|                     if v.records_number == 0 { | ||||
|                         log::debug!(target: "api", "BNF returned 0 records for fetch"); | ||||
|                         return None; | ||||
|                     } | ||||
|                     let data_dubfield = v.records.first().unwrap().record_data.record.record.datafields.iter().find(|d| d.tag == "200").unwrap().subfields.clone(); | ||||
|                     Some(FetchedBook { | ||||
|                         ean: ean.to_string(), | ||||
|                         title: data_dubfield.iter().find(|p| p.code == "a").unwrap().value.clone(), | ||||
|                         author: data_dubfield.iter().find(|p| p.code == "f").unwrap().value.clone(), | ||||
|                     }) | ||||
|                 }, | ||||
|                 Err(e) => { | ||||
|                     log::debug!(target: "api", "Error while deserializing: {e:?}"); | ||||
|                     None | ||||
|                 } | ||||
|             } | ||||
|         }, | ||||
|         _ => None | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| //https://catalogue.bnf.fr/api/SRU?version=1.2&operation=searchRetrieve&query=bib.isbn%20any%20%{ean}%22
 | ||||
|  | @ -1,5 +1,4 @@ | |||
| pub mod auth; | ||||
| pub mod bnf; | ||||
| pub mod cli; | ||||
| pub mod events; | ||||
| pub mod open_library; | ||||
|  |  | |||
		Reference in a new issue