Compare commits

..

3 commits

9 changed files with 175 additions and 14 deletions

11
Cargo.lock generated
View file

@ -53,6 +53,7 @@ dependencies = [
"openssl",
"password-hash",
"pretty_env_logger",
"quick-xml",
"rand_core 0.9.3",
"reqwest",
"sea-orm",
@ -2082,6 +2083,16 @@ 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"

View file

@ -25,4 +25,5 @@ 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"] }

View file

@ -170,6 +170,7 @@ 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))

View file

@ -57,7 +57,7 @@ pub struct BalCreateParams {
request_body = BalCreateParams,
security(("jwt" = [])),
responses(
(status = OK, body = bal::Model, description = "Successfully created BAL"),
(status = CREATED, 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::OK, Json(Some(res.try_into_model().expect("All fields should be set once the bal is saved"))))
Ok(res) => (StatusCode::CREATED, Json(Some(res.try_into_model().expect("All fields should be set once the bal is saved"))))
}
}

View file

@ -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::open_library};
use crate::{entities::{book, prelude::Book}, utils::{bnf}};
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 = open_library::fetch_book_by_ean(&state.web_client, &ean).await;
let fetched_book = bnf::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 = OK, body = book::Model, description = "Successfully saved book data"),
(status = CREATED, 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::OK, Json(Some(res.try_into_model().expect("All fields should be set once the book is saved"))))
Ok(res) => (StatusCode::CREATED, Json(Some(res.try_into_model().expect("All fields should be set once the book is saved"))))
}
}

View file

@ -2,11 +2,11 @@ use std::sync::Arc;
use axum::{extract::{Path, State}, Json};
use reqwest::{StatusCode};
use sea_orm::{ActiveModelTrait, ActiveValue::{NotSet, Set}, EntityTrait, TryIntoModel};
use sea_orm::{ActiveModelTrait, ActiveValue::{NotSet, Set}, ColumnTrait, EntityTrait, QueryFilter, 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}, AppState};
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};
#[derive(IntoParams)]
@ -59,7 +59,7 @@ pub struct BookInstanceCreateParams {
request_body = BookInstanceCreateParams,
security(("jwt" = [])),
responses(
(status = OK, body = book_instance::Model, description = "Successfully created book instance"),
(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",
@ -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::OK, Json(Some(res.try_into_model().expect("All fields should be set once the book instance is saved"))))
Ok(res) => (StatusCode::CREATED, 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 = OK, description = "Successfully created book instances"),
(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",
@ -253,6 +253,45 @@ 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::OK
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![]))
}
}

View file

@ -59,7 +59,7 @@ pub struct OwnerCreateParams {
request_body = OwnerCreateParams,
security(("jwt" = [])),
responses(
(status = OK, body = owner::Model, description = "Successfully created owner"),
(status = CREATED, 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::OK, Json(Some(model)))
(StatusCode::CREATED, Json(Some(model)))
}
}
}

108
src/utils/bnf.rs Normal file
View file

@ -0,0 +1,108 @@
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

View file

@ -1,4 +1,5 @@
pub mod auth;
pub mod bnf;
pub mod cli;
pub mod events;
pub mod open_library;