From c667608fbb6ebe23af825c101f701b426c6f44a6 Mon Sep 17 00:00:00 2001 From: Ninjdai Date: Mon, 11 Aug 2025 11:15:27 +0200 Subject: [PATCH] feat: bal state --- src/entities/bal.rs | 12 +++ src/entities/user.rs | 1 - src/lib.rs | 4 +- src/routes/bal.rs | 190 ++++++++++++++++++++-------------------- src/routes/websocket.rs | 1 - src/utils/cli.rs | 1 - tests/common/mod.rs | 1 - 7 files changed, 109 insertions(+), 101 deletions(-) diff --git a/src/entities/bal.rs b/src/entities/bal.rs index 88b0afb..b5f2080 100644 --- a/src/entities/bal.rs +++ b/src/entities/bal.rs @@ -9,10 +9,22 @@ pub struct Model { pub id: u32, pub user_id: u32, pub name: String, + pub state: BalState, pub start_timestamp: i64, pub end_timestamp: i64 } +#[derive(Clone, Debug, PartialEq, Eq, EnumIter, DeriveActiveEnum, Serialize, Deserialize, utoipa::ToSchema)] +#[sea_orm(rs_type = "String", db_type = "String(StringLen::N(1))")] +pub enum BalState { + #[sea_orm(string_value = "P")] + Pending, + #[sea_orm(string_value = "O")] + Ongoing, + #[sea_orm(string_value = "E")] + Ended +} + #[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)] pub enum Relation { #[sea_orm(has_many = "super::book_instance::Entity")] diff --git a/src/entities/user.rs b/src/entities/user.rs index 20670c2..4efcba9 100644 --- a/src/entities/user.rs +++ b/src/entities/user.rs @@ -11,7 +11,6 @@ pub struct Model { pub username: String, pub hashed_password: String, pub owner_id: Option, - pub current_bal_id: Option, } impl Model { diff --git a/src/lib.rs b/src/lib.rs index 4c824de..42da2bc 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -170,9 +170,9 @@ pub async fn run_server(db: Arc, port: u16, serve_docs: bool .routes(routes!(routes::bal::get_bal_by_id)) .routes(routes!(routes::bal::create_bal)) .routes(routes!(routes::bal::update_bal)) + .routes(routes!(routes::bal::start_bal)) + .routes(routes!(routes::bal::stop_bal)) .routes(routes!(routes::bal::get_bals)) - .routes(routes!(routes::bal::get_current_bal)) - .routes(routes!(routes::bal::set_current_bal)) // Authentication .route_layer(middleware::from_fn_with_state(shared_state.clone(), routes::auth::auth_middleware)) .routes(routes!(routes::auth::auth)) diff --git a/src/routes/bal.rs b/src/routes/bal.rs index 1e2e0ac..585aa06 100644 --- a/src/routes/bal.rs +++ b/src/routes/bal.rs @@ -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, IntoActiveModel, QueryFilter, TryIntoModel}; +use sea_orm::{ActiveModelTrait, ActiveValue::{NotSet, Set}, ColumnTrait, EntityTrait, QueryFilter, TryIntoModel}; use serde::{Deserialize, Serialize}; use utoipa::IntoParams; -use crate::{entities::{bal, book_instance, prelude::*, user}, routes::auth::Claims, AppState}; +use crate::{entities::{bal::{self, BalState}, prelude::*}, routes::auth::Claims, AppState}; #[derive(IntoParams)] @@ -74,6 +74,7 @@ pub async fn create_bal( id: NotSet, user_id: Set(claims.user_id), name: Set(instance_payload.name), + state: Set(BalState::Pending), start_timestamp: Set(instance_payload.start_timestamp), end_timestamp: Set(instance_payload.end_timestamp), }; @@ -150,6 +151,98 @@ pub async fn update_bal( } } +#[axum::debug_handler] +#[utoipa::path( + post, + path = "/bal/{id}/start", + params(BalByIdParams), + security(("jwt" = [])), + responses( + (status = OK, body = bal::Model, description = "Successfully started bal"), + (status = CONFLICT, description = "The specified BAL was not in a Pending state, cannot start it"), + (status = NOT_FOUND, description = "No bal with specified id was found"), + (status = FORBIDDEN, description = "You don't own the specified bal"), + ), + summary = "Start a bal", + description = "Start a bal", + tag = "bal-api", +)] +pub async fn start_bal( + State(state): State>, + claims: Claims, + Path(id): Path, +) -> (StatusCode, Json>) { + 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)); + } + if bal.state != BalState::Pending { + return (StatusCode::CONFLICT, Json(None)); + } + let mut bal: bal::ActiveModel = bal.into(); + bal.state = Set(BalState::Ongoing); + + 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( + post, + path = "/bal/{id}/stop", + params(BalByIdParams), + security(("jwt" = [])), + responses( + (status = OK, body = bal::Model, description = "Successfully stopped bal"), + (status = CONFLICT, description = "The specified BAL was not in an Ongoing state, cannot stop it"), + (status = NOT_FOUND, description = "No bal with specified id was found"), + (status = FORBIDDEN, description = "You don't own the specified bal"), + ), + summary = "Stop a bal", + description = "Stop a bal", + tag = "bal-api", +)] +pub async fn stop_bal( + State(state): State>, + claims: Claims, + Path(id): Path, +) -> (StatusCode, Json>) { + 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)); + } + if bal.state != BalState::Ongoing { + return (StatusCode::CONFLICT, Json(None)); + } + let mut bal: bal::ActiveModel = bal.into(); + bal.state = Set(BalState::Ended); + + 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, @@ -173,96 +266,3 @@ pub async fn get_bals( } } -#[axum::debug_handler] -#[utoipa::path( - get, - path = "/bal/current", - security(("jwt" = [])), - responses( - (status = OK, body = bal::Model, description = "Your current active BAL"), - (status = NOT_FOUND, description = "You don't have a currently active BAL"), - ), - summary = "Get your current active BAL", - description = "Get your current active BAL", - tag = "bal-api", -)] -pub async fn get_current_bal( - State(state): State>, - claims: Claims, -) -> (StatusCode, Json>) { - if let Ok(Some(user)) = User::find_by_id(claims.user_id).one(state.db_conn.as_ref()).await { - if let Some(bal_id) = user.current_bal_id { - (StatusCode::OK, Json(Some(Bal::find_by_id(bal_id).one(state.db_conn.as_ref()).await.unwrap().unwrap()))) - } else { - (StatusCode::NOT_FOUND, Json(None)) - } - } else { - (StatusCode::INTERNAL_SERVER_ERROR, Json(None)) - } -} - -#[derive(Deserialize, PartialEq, utoipa::ToSchema)] -pub enum BookInstanceTransferMode { - All, - None, - OwnedOnly -} - -#[derive(Deserialize, utoipa::ToSchema)] -pub struct SetCurrentBalParams{ - bal_id: u32, - transfer_book_instances: Option -} - -#[axum::debug_handler] -#[utoipa::path( - post, - path = "/bal/current", - request_body = SetCurrentBalParams, - security(("jwt" = [])), - responses( - (status = OK, description = "Successfully set current active BAL"), - (status = UNAUTHORIZED, description = "Tried to set a BAL you don't own as your active BAL"), - ), - summary = "Set your current active BAL", - description = "Set your current active BAL", - tag = "bal-api", -)] -pub async fn set_current_bal( - State(state): State>, - claims: Claims, - Json(payload): Json, -) -> StatusCode { - if let Ok(Some(new_bal)) = Bal::find_by_id(payload.bal_id).one(state.db_conn.as_ref()).await - && let Ok(Some(user)) = User::find_by_id(claims.user_id).one(state.db_conn.as_ref()).await - { - if new_bal.user_id != claims.user_id { - return StatusCode::UNAUTHORIZED; - } - - if let Some(old_bal_id) = user.current_bal_id { - // Optional instance transfer - if let Some(mode) = payload.transfer_book_instances && mode != BookInstanceTransferMode::None { - let mut update_query = BookInstance::update_many() - .set(book_instance::ActiveModel { - bal_id: Set(new_bal.id), - ..Default::default() - }) - .filter(book_instance::Column::BalId.eq(old_bal_id)); - if mode == BookInstanceTransferMode::OwnedOnly { - update_query = update_query.filter(book_instance::Column::OwnerId.eq(user.owner_id)); - } - - let _ = update_query.exec(state.db_conn.as_ref()).await; - } - } - - // Set current bal on user - let mut user_active_model: user::ActiveModel = user.into_active_model(); - user_active_model.current_bal_id = Set(Some(payload.bal_id)); - let _ = User::update(user_active_model).exec(state.db_conn.as_ref()).await; - StatusCode::OK - } else { - return StatusCode::NOT_FOUND; - } -} diff --git a/src/routes/websocket.rs b/src/routes/websocket.rs index 33aad9a..6d25022 100644 --- a/src/routes/websocket.rs +++ b/src/routes/websocket.rs @@ -16,7 +16,6 @@ use futures_util::{sink::SinkExt, stream::StreamExt}; #[utoipa::path( get, path = "/ws", - security(("jwt" = [])), responses( (status = SWITCHING_PROTOCOLS, description = "Succesfully reached the websocket, now upgrade to establish the connection"), ), diff --git a/src/utils/cli.rs b/src/utils/cli.rs index dbc0097..483067f 100644 --- a/src/utils/cli.rs +++ b/src/utils/cli.rs @@ -63,7 +63,6 @@ pub async fn manage_users(db: Arc) { id: NotSet, username: Set(username), hashed_password: Set(hash_password(password)), - current_bal_id: Set(None), owner_id: Set(None) }; let res = new_user.clone().insert(db.as_ref()).await.unwrap(); diff --git a/tests/common/mod.rs b/tests/common/mod.rs index 1831471..02e24a7 100644 --- a/tests/common/mod.rs +++ b/tests/common/mod.rs @@ -67,7 +67,6 @@ where C: ConnectionTrait { id: NotSet, username: Set(username.clone()), hashed_password: Set(hash_password(password)), - current_bal_id: Set(None), owner_id: Set(None) }; let res = new_user.clone().insert(db_conn).await.unwrap();