diff --git a/src/entities/bal.rs b/src/entities/bal.rs new file mode 100644 index 0000000..845d80a --- /dev/null +++ b/src/entities/bal.rs @@ -0,0 +1,37 @@ +use sea_orm::entity::prelude::*; +use serde::{Deserialize, Serialize}; + +#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Eq, Serialize, Deserialize, utoipa::ToSchema)] +#[sea_orm(table_name = "BAL")] +#[schema(title="Book", as=entities::BAL)] +pub struct Model { + #[sea_orm(primary_key, auto_increment = true)] + pub id: u32, + pub user_id: u32, + pub name: String, +} + +#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)] +pub enum Relation { + #[sea_orm(has_many = "super::book_instance::Entity")] + BookInstance, +} + +impl Related for Entity { + fn to() -> RelationDef { + Relation::BookInstance.def() + } +} + +impl Entity { + pub async fn get_by_id(db_conn: &C, id: u32) -> Option + where C: ConnectionTrait, + { + match Self::find_by_id(id).one(db_conn).await { + Ok(res) => res, + Err(_) => None + } + } +} + +impl ActiveModelBehavior for ActiveModel {} diff --git a/src/entities/book_instance.rs b/src/entities/book_instance.rs index 1938082..e2e213b 100644 --- a/src/entities/book_instance.rs +++ b/src/entities/book_instance.rs @@ -11,9 +11,20 @@ pub struct Model { pub sold_price: Option, pub status: BookStatus, pub book_id: u32, - pub owner_id: u32 + pub owner_id: u32, + pub bal_id: u32 +} + +impl Entity { + pub async fn get_by_id(db_conn: &C, id: u32) -> Option + where C: ConnectionTrait, + { + match Self::find_by_id(id).one(db_conn).await { + Ok(res) => res, + Err(_) => None + } + } } -// Missing: Bal #[derive(EnumIter, DeriveActiveEnum, PartialEq, Eq ,Deserialize, Serialize, Clone, Copy, Debug, utoipa::ToSchema)] #[sea_orm(rs_type = "String", db_type = "String(StringLen::N(1))")] @@ -40,6 +51,12 @@ pub enum Relation { to = "super::owner::Column::Id" )] Owner, + #[sea_orm( + belongs_to = "super::bal::Entity", + from = "Column::BalId", + to = "super::bal::Column::Id" + )] + Bal, } impl Related for Entity { @@ -54,4 +71,10 @@ impl Related for Entity { } } +impl Related for Entity { + fn to() -> RelationDef { + Relation::Bal.def() + } +} + impl ActiveModelBehavior for ActiveModel {} diff --git a/src/entities/mod.rs b/src/entities/mod.rs index 9838383..fd63ca8 100644 --- a/src/entities/mod.rs +++ b/src/entities/mod.rs @@ -1,5 +1,6 @@ pub mod prelude; +pub mod bal; pub mod book; pub mod book_instance; pub mod owner; diff --git a/src/entities/owner.rs b/src/entities/owner.rs index 6bdb775..a95fedb 100644 --- a/src/entities/owner.rs +++ b/src/entities/owner.rs @@ -7,15 +7,33 @@ use serde::{Deserialize, Serialize}; pub struct Model { #[sea_orm(primary_key, auto_increment = true)] pub id: u32, + pub user_id: u32, pub first_name: String, pub last_name: String, pub contact: String } +impl Entity { + pub async fn get_by_id(db_conn: &C, id: u32) -> Option + where C: ConnectionTrait, + { + match Self::find_by_id(id).one(db_conn).await { + Ok(res) => res, + Err(_) => None + } + } +} + #[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)] pub enum Relation { #[sea_orm(has_many = "super::book_instance::Entity")] - BookInstance + BookInstance, + #[sea_orm( + belongs_to = "super::user::Entity", + from = "Column::UserId", + to = "super::user::Column::Id" + )] + User, } impl Related for Entity { @@ -24,4 +42,10 @@ impl Related for Entity { } } +impl Related for Entity { + fn to() -> RelationDef { + Relation::User.def() + } +} + impl ActiveModelBehavior for ActiveModel {} diff --git a/src/entities/prelude.rs b/src/entities/prelude.rs index 514f26d..a4e87e2 100644 --- a/src/entities/prelude.rs +++ b/src/entities/prelude.rs @@ -1,4 +1,5 @@ +pub use super::bal::Entity as Bal; pub use super::book::Entity as Book; pub use super::book_instance::Entity as BookInstance; pub use super::owner::Entity as Owner; diff --git a/src/entities/user.rs b/src/entities/user.rs index e8145e4..f0b2ad0 100644 --- a/src/entities/user.rs +++ b/src/entities/user.rs @@ -21,6 +21,14 @@ impl Model { #[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)] pub enum Relation { + #[sea_orm(has_many = "super::owner::Entity")] + Owner, +} + +impl Related for Entity { + fn to() -> RelationDef { + Relation::Owner.def() + } } impl ActiveModelBehavior for ActiveModel {} diff --git a/src/routes/auth.rs b/src/routes/auth.rs index 9bf3841..e2ea921 100644 --- a/src/routes/auth.rs +++ b/src/routes/auth.rs @@ -53,6 +53,7 @@ pub async fn auth(State(state): State>, Json(payload): Json>, + 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), status: Set(book_instance::BookStatus::Available), ..Default::default() @@ -99,6 +106,7 @@ pub struct BookInstanceUpdateParams { 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", @@ -106,9 +114,13 @@ pub struct BookInstanceUpdateParams { )] 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(); @@ -162,9 +174,13 @@ pub struct BookInstanceSaleParams { )] 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 { let mut book_instance: book_instance::ActiveModel = book_instance.into(); @@ -185,6 +201,7 @@ pub async fn sell_book_instance( (StatusCode::NOT_FOUND, Json(None)) } } + #[axum::debug_handler] #[utoipa::path( post, @@ -193,6 +210,7 @@ pub async fn sell_book_instance( security(("jwt" = [])), responses( (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", description = "Create new book instances in bulk", @@ -200,17 +218,27 @@ pub async fn sell_book_instance( )] 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 + let instances = instance_payload .into_iter() - .map(|p| book_instance::ActiveModel { - book_id: Set(p.book_id), - owner_id: Set(p.owner_id), - price: Set(p.price), - status: Set(book_instance::BookStatus::Available), - ..Default::default() + .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), + status: Set(book_instance::BookStatus::Available), + id: NotSet, + sold_price: NotSet + } }); match BookInstance::insert_many(instances).exec(state.db_conn.as_ref()).await { diff --git a/src/routes/owner.rs b/src/routes/owner.rs index b067746..35dc212 100644 --- a/src/routes/owner.rs +++ b/src/routes/owner.rs @@ -6,7 +6,7 @@ use sea_orm::{ActiveModelTrait, ActiveValue::{NotSet, Set, Unchanged}, EntityTra use serde::{Deserialize, Serialize}; use utoipa::IntoParams; -use crate::{entities::{owner, prelude::Owner}, utils::events::{Event, WebsocketMessage}, AppState}; +use crate::{entities::{owner, prelude::Owner}, routes::auth::Claims, utils::events::{Event, WebsocketMessage}, AppState}; #[derive(IntoParams)] @@ -61,6 +61,7 @@ pub struct OwnerCreateParams { )] pub async fn create_owner( State(state): State>, + claims: Claims, Json(instance_payload): Json, ) -> (StatusCode, Json>) { @@ -68,6 +69,7 @@ pub async fn create_owner( first_name: Set(instance_payload.first_name), last_name: Set(instance_payload.last_name), contact: Set(instance_payload.contact), + user_id: Set(claims.user_id), id: NotSet }; @@ -101,7 +103,8 @@ pub struct OwnerUpdateParams { security(("jwt" = [])), responses( (status = OK, body = owner::Model, description = "Successfully updated owner"), - (status = NOT_FOUND, description = "No owner with this id exists in the database") + (status = NOT_FOUND, description = "No owner with this id exists in the database"), + (status = FORBIDDEN, description = "The owner doesn't belong to this account"), ), summary = "Update an owner", description = "Update an owner", @@ -109,11 +112,15 @@ pub struct OwnerUpdateParams { )] pub async fn update_owner( State(state): State>, + claims: Claims, Path(id): Path, Json(instance_payload): Json, ) -> (StatusCode, Json>) { if let Ok(Some(owner)) = Owner::find_by_id(id).one(state.db_conn.as_ref()).await { + if owner.user_id != claims.user_id { + return (StatusCode::FORBIDDEN, Json(None)); + } let mut owner: owner::ActiveModel = owner.into(); owner.first_name = match instance_payload.first_name { None => owner.first_name, diff --git a/src/routes/websocket.rs b/src/routes/websocket.rs index ee14c99..8cf8d98 100644 --- a/src/routes/websocket.rs +++ b/src/routes/websocket.rs @@ -8,7 +8,7 @@ use axum::{ }, response::IntoResponse }; -use crate::{utils::events, AppState}; +use crate::{routes::auth::Claims, utils::events, AppState}; use futures_util::{sink::SinkExt, stream::StreamExt}; @@ -27,14 +27,15 @@ use futures_util::{sink::SinkExt, stream::StreamExt}; pub async fn ws_handler( ws: WebSocketUpgrade, ConnectInfo(addr): ConnectInfo, + claims: Claims, State(state): State> ) -> impl IntoResponse { log::debug!(target: "websocket", "{addr} connected."); - ws.on_upgrade(move |socket| handle_socket(socket, addr, state)) + ws.on_upgrade(move |socket| handle_socket(socket, addr, state, claims)) } -async fn handle_socket(mut socket: WebSocket, who: SocketAddr, state: Arc) { +async fn handle_socket(mut socket: WebSocket, who: SocketAddr, state: Arc, claims: Claims) { if socket .send(Message::Ping(Bytes::from_static(&[4, 2]))) .await @@ -73,6 +74,9 @@ async fn handle_socket(mut socket: WebSocket, who: SocketAddr, state: Arc { match event { events::Event::WebsocketBroadcast(message) => { + if !message.should_user_receive(claims.user_id) { + continue; + }; log::debug!(target: "websocket", "Sent {message:?} to {who}"); let _ = sender.send(Message::Text(Utf8Bytes::from(message.to_json().to_string()))).await; } diff --git a/src/utils/auth.rs b/src/utils/auth.rs new file mode 100644 index 0000000..1bed6e5 --- /dev/null +++ b/src/utils/auth.rs @@ -0,0 +1,42 @@ +use sea_orm::ConnectionTrait; + +use crate::entities::prelude::*; + +pub async fn user_is_bal_owner( + user_id: u32, + bal_id: u32, + db_conn: &C +) -> bool +where C: ConnectionTrait, +{ + match Bal::get_by_id(db_conn, bal_id).await { + Some(bal) => user_id == bal.user_id, + None => false + } +} + +pub async fn user_is_book_instance_owner( + user_id: u32, + book_instance_id: u32, + db_conn: &C +) -> bool +where C: ConnectionTrait, +{ + match BookInstance::get_by_id(db_conn, book_instance_id).await { + Some(instance) => user_is_bal_owner(user_id, instance.bal_id, db_conn).await, + None => false + } +} + +pub async fn user_is_owner_owner( + user_id: u32, + owner_id: u32, + db_conn: &C +) -> bool +where C: ConnectionTrait, +{ + match Owner::get_by_id(db_conn, owner_id).await { + Some(owner) => owner.user_id == user_id, + None => false + } +} diff --git a/src/utils/events.rs b/src/utils/events.rs index 1715fa3..56d7381 100644 --- a/src/utils/events.rs +++ b/src/utils/events.rs @@ -11,18 +11,28 @@ pub enum Event { #[derive(Clone, Debug)] pub enum WebsocketMessage { - NewOwner(Arc) + NewOwner(Arc), + Ping } impl WebsocketMessage { - pub fn to_json(self) -> Value { + pub fn to_json(&self) -> Value { json!({ "type": match self { Self::NewOwner(_) => "new_owner", + Self::Ping => "ping", }, "data": match self { Self::NewOwner(owner) => json!(owner), + Self::Ping => json!(null), } }) } + + pub fn should_user_receive(&self, user_id: u32) -> bool { + match self { + Self::NewOwner(owner) => owner.user_id == user_id, + _ => true + } + } } diff --git a/src/utils/mod.rs b/src/utils/mod.rs index 3733d86..8068292 100644 --- a/src/utils/mod.rs +++ b/src/utils/mod.rs @@ -1,3 +1,4 @@ +pub mod auth; pub mod cli; pub mod events; pub mod open_library;