Compare commits

...

3 commits

Author SHA1 Message Date
Ninjdai
c36a38cd7a feat: BAL API 2025-08-03 23:12:38 +02:00
Ninjdai
a657d672be check user permissions for book instance API 2025-08-03 23:11:15 +02:00
Ninjdai
9fb527f9df check user permissions for owner API 2025-08-03 23:08:28 +02:00
5 changed files with 196 additions and 11 deletions

View file

@ -107,6 +107,10 @@ async fn main() {
log::error!(target: "database", "Error while creating user table: {err:?}"); log::error!(target: "database", "Error while creating user table: {err:?}");
return; return;
} }
if let Err(err) = db.execute(builder.build(schema.create_table_from_entity(crate::entities::prelude::Bal).if_not_exists())).await {
log::error!(target: "database", "Error while creating bal table: {err:?}");
return;
}
match cli.command { match cli.command {
Commands::Run => run_server(db).await, Commands::Run => run_server(db).await,
@ -171,6 +175,11 @@ async fn run_server(db: Arc<DatabaseConnection>) {
.routes(routes!(routes::owner::create_owner)) .routes(routes!(routes::owner::create_owner))
.routes(routes!(routes::owner::update_owner)) .routes(routes!(routes::owner::update_owner))
.routes(routes!(routes::owner::get_owners)) .routes(routes!(routes::owner::get_owners))
// Bal API
.routes(routes!(routes::bal::get_bal_by_id))
.routes(routes!(routes::bal::create_bal))
.routes(routes!(routes::bal::update_bal))
.routes(routes!(routes::bal::get_bals))
// Misc // Misc
.routes(routes!(routes::websocket::ws_handler)) .routes(routes!(routes::websocket::ws_handler))
// Authentication // Authentication

161
src/routes/bal.rs Normal file
View file

@ -0,0 +1,161 @@
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 serde::{Deserialize, Serialize};
use utoipa::IntoParams;
use crate::{entities::{bal, prelude::*}, routes::auth::Claims, AppState};
#[derive(IntoParams)]
#[into_params(names("id"), parameter_in = Path)]
#[allow(dead_code)]
struct BalByIdParams(u32);
#[axum::debug_handler]
#[utoipa::path(
get,
path = "/bal/{id}",
params(BalByIdParams),
security(("jwt" = [])),
responses(
(status = OK, body = bal::Model, description = "Found bal with corresponding ID in the database"),
(status = NOT_FOUND, description = "No bal with this id exists in the database"),
(status = FORBIDDEN, description = "You don't own the specified bal"),
),
summary = "Get a bal by its ID",
description = "Get a bal from its ID",
tag = "bal-api",
)]
pub async fn get_bal_by_id(
State(state): State<Arc<AppState>>,
claims: Claims,
Path(id): Path<u32>,
) -> (StatusCode, Json<Option<bal::Model>>) {
if let Ok(Some(res)) = Bal::find_by_id(id).one(state.db_conn.as_ref()).await {
if !res.user_id == claims.user_id {
(StatusCode::FORBIDDEN, Json(None))
} else {
(StatusCode::OK, Json(Some(res)))
}
} else {
(StatusCode::NOT_FOUND, Json(None))
}
}
#[derive(Deserialize, Serialize, utoipa::ToSchema)]
pub struct BalCreateParams {
name: String,
}
#[axum::debug_handler]
#[utoipa::path(
post,
path = "/bal",
request_body = BalCreateParams,
security(("jwt" = [])),
responses(
(status = OK, body = bal::Model, description = "Successfully created BAL"),
),
summary = "Create a new bal",
description = "Create a new bal",
tag = "bal-api",
)]
pub async fn create_bal(
State(state): State<Arc<AppState>>,
claims: Claims,
Json(instance_payload): Json<BalCreateParams>,
) -> (StatusCode, Json<Option<bal::Model>>) {
let bal = bal::ActiveModel {
id: NotSet,
user_id: Set(claims.user_id),
name: Set(instance_payload.name),
};
let b = bal.save(state.db_conn.as_ref()).await;
match b {
Err(e) => {
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"))))
}
}
#[derive(Deserialize, Serialize, utoipa::ToSchema)]
pub struct BalUpdateParams {
name: Option<String>,
}
#[axum::debug_handler]
#[utoipa::path(
patch,
path = "/bal/{id}",
params(BalByIdParams),
request_body = BalUpdateParams,
security(("jwt" = [])),
responses(
(status = OK, body = bal::Model, description = "Successfully updated bal"),
(status = NOT_FOUND, description = "No bal with specified id was found"),
(status = FORBIDDEN, description = "You don't own the specified bal"),
),
summary = "Update a bal",
description = "Update a bal",
tag = "bal-api",
)]
pub async fn update_bal(
State(state): State<Arc<AppState>>,
claims: Claims,
Path(id): Path<u32>,
Json(instance_payload): Json<BalUpdateParams>,
) -> (StatusCode, Json<Option<bal::Model>>) {
if let Ok(Some(bal)) = Bal::find_by_id(id).one(state.db_conn.as_ref()).await {
if bal.user_id != claims.user_id {
return (StatusCode::FORBIDDEN, Json(None));
}
let mut bal: bal::ActiveModel = bal.into();
bal.name = match instance_payload.name {
None => bal.name,
Some(v) => Set(v)
};
match bal.update(state.db_conn.as_ref()).await {
Err(e) => {
log::error!(target: "api", "Error while updating bal 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 bal is saved");
(StatusCode::OK, Json(Some(model)))
}
}
} else {
(StatusCode::NOT_FOUND, Json(None))
}
}
#[axum::debug_handler]
#[utoipa::path(
get,
path = "/bals",
params(BalByIdParams),
security(("jwt" = [])),
responses(
(status = OK, body = Vec<bal::Model>, description = "All Bals you own"),
),
summary = "Get all Bals you own",
description = "Get all Bals you own",
tag = "bal-api",
)]
pub async fn get_bals(
State(state): State<Arc<AppState>>,
claims: Claims,
) -> (StatusCode, Json<Vec<bal::Model>>) {
if let Ok(res) = Bal::find().filter(bal::Column::UserId.eq(claims.user_id)).all(state.db_conn.as_ref()).await {
(StatusCode::OK, Json(res))
} else {
(StatusCode::NOT_FOUND, Json(vec![]))
}
}

View file

@ -22,7 +22,8 @@ struct BookInstanceByIdParams(u32);
security(("jwt" = [])), security(("jwt" = [])),
responses( responses(
(status = OK, body = book_instance::Model, description = "Found book instance with corresponding ID in the database"), (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 = 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", summary = "Get a book instance by its ID",
description = "Get a book instance from its ID", description = "Get a book instance from its ID",
@ -30,9 +31,13 @@ struct BookInstanceByIdParams(u32);
)] )]
pub async fn get_book_instance_by_id( pub async fn get_book_instance_by_id(
State(state): State<Arc<AppState>>, State(state): State<Arc<AppState>>,
claims: Claims,
Path(id): Path<u32>, Path(id): Path<u32>,
) -> (StatusCode, Json<Option<book_instance::Model>>) { ) -> (StatusCode, Json<Option<book_instance::Model>>) {
if let Ok(Some(res)) = BookInstance::find_by_id(id).one(state.db_conn.as_ref()).await { 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))) (StatusCode::OK, Json(Some(res)))
} else { } else {
(StatusCode::NOT_FOUND, Json(None)) (StatusCode::NOT_FOUND, Json(None))
@ -55,7 +60,7 @@ pub struct BookInstanceCreateParams {
security(("jwt" = [])), security(("jwt" = [])),
responses( responses(
(status = OK, 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 BAL"), (status = FORBIDDEN, description = "You don't own the specified book instance"),
), ),
summary = "Create a new book instance", summary = "Create a new book instance",
description = "Create a new book instance", description = "Create a new book instance",
@ -76,7 +81,8 @@ pub async fn create_book_instance(
bal_id: Set(instance_payload.bal_id), bal_id: Set(instance_payload.bal_id),
price: Set(instance_payload.price), price: Set(instance_payload.price),
status: Set(book_instance::BookStatus::Available), status: Set(book_instance::BookStatus::Available),
..Default::default() id: NotSet,
sold_price: NotSet,
}; };
let b = book_instance.save(state.db_conn.as_ref()).await; let b = book_instance.save(state.db_conn.as_ref()).await;
@ -167,6 +173,7 @@ pub struct BookInstanceSaleParams {
responses( responses(
(status = OK, body = book_instance::Model, description = "Successfully sold book instance"), (status = OK, body = book_instance::Model, description = "Successfully sold book instance"),
(status = NOT_FOUND, description = "No book instance with specified id was found"), (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", summary = "Sell a book instance",
description = "Sell a book instance", description = "Sell a book instance",

View file

@ -1,4 +1,5 @@
pub mod auth; pub mod auth;
pub mod bal;
pub mod book; pub mod book;
pub mod book_instance; pub mod book_instance;
pub mod owner; pub mod owner;

View file

@ -2,7 +2,7 @@ use std::sync::Arc;
use axum::{extract::{Path, State}, Json}; use axum::{extract::{Path, State}, Json};
use reqwest::{StatusCode}; use reqwest::{StatusCode};
use sea_orm::{ActiveModelTrait, ActiveValue::{NotSet, Set, Unchanged}, EntityTrait, TryIntoModel}; use sea_orm::{ActiveModelTrait, ActiveValue::{NotSet, Set}, ColumnTrait, EntityTrait, QueryFilter, TryIntoModel};
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use utoipa::IntoParams; use utoipa::IntoParams;
@ -22,7 +22,8 @@ struct OwnerByIdParams(u32);
security(("jwt" = [])), security(("jwt" = [])),
responses( responses(
(status = OK, body = owner::Model, description = "Found owner with corresponding ID in the database"), (status = OK, body = owner::Model, description = "Found owner with corresponding ID in the database"),
(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 = "You do not own the specified owner"),
), ),
summary = "Get an owner by its ID", summary = "Get an owner by its ID",
description = "Get an owner from its ID", description = "Get an owner from its ID",
@ -30,10 +31,15 @@ struct OwnerByIdParams(u32);
)] )]
pub async fn get_owner_by_id( pub async fn get_owner_by_id(
State(state): State<Arc<AppState>>, State(state): State<Arc<AppState>>,
claims: Claims,
Path(id): Path<u32>, Path(id): Path<u32>,
) -> (StatusCode, Json<Option<owner::Model>>) { ) -> (StatusCode, Json<Option<owner::Model>>) {
if let Ok(Some(res)) = Owner::find_by_id(id).one(state.db_conn.as_ref()).await { if let Ok(Some(res)) = Owner::find_by_id(id).one(state.db_conn.as_ref()).await {
if res.user_id != claims.user_id {
(StatusCode::FORBIDDEN, Json(None))
} else {
(StatusCode::OK, Json(Some(res))) (StatusCode::OK, Json(Some(res)))
}
} else { } else {
(StatusCode::NOT_FOUND, Json(None)) (StatusCode::NOT_FOUND, Json(None))
} }
@ -165,14 +171,15 @@ pub async fn update_owner(
)] )]
pub async fn get_owners( pub async fn get_owners(
State(state): State<Arc<AppState>>, State(state): State<Arc<AppState>>,
) -> (StatusCode, Json<Option<Vec<owner::Model>>>) { claims: Claims
match Owner::find().all(state.db_conn.as_ref()).await { ) -> (StatusCode, Json<Vec<owner::Model>>) {
match Owner::find().filter(owner::Column::UserId.eq(claims.user_id)).all(state.db_conn.as_ref()).await {
Err(e) => { Err(e) => {
log::error!(target: "api", "Error while getting owner list: {:#?}", e); log::error!(target: "api", "Error while getting owner list: {:#?}", e);
(StatusCode::INTERNAL_SERVER_ERROR, Json(None)) (StatusCode::INTERNAL_SERVER_ERROR, Json(vec![]))
} }
Ok(owners) => { Ok(owners) => {
(StatusCode::OK, Json(Some(owners))) (StatusCode::OK, Json(owners))
} }
} }
} }