From b4cf6624f77494882d1362a3417081b57cda89dd Mon Sep 17 00:00:00 2001 From: Pierre HUBERT Date: Tue, 15 Apr 2025 22:39:42 +0200 Subject: [PATCH] Can create a movement --- moneymgr_backend/src/controllers/mod.rs | 1 + .../src/controllers/movement_controller.rs | 16 ++++ .../src/controllers/server_controller.rs | 2 + moneymgr_backend/src/main.rs | 2 + moneymgr_backend/src/models/mod.rs | 1 + moneymgr_backend/src/models/movements.rs | 47 ++++++++++ moneymgr_backend/src/services/mod.rs | 1 + .../src/services/movements_service.rs | 89 +++++++++++++++++++ moneymgr_web/src/api/ServerApi.ts | 1 + 9 files changed, 160 insertions(+) create mode 100644 moneymgr_backend/src/controllers/movement_controller.rs create mode 100644 moneymgr_backend/src/models/movements.rs create mode 100644 moneymgr_backend/src/services/movements_service.rs diff --git a/moneymgr_backend/src/controllers/mod.rs b/moneymgr_backend/src/controllers/mod.rs index 49c3510..1bb254c 100644 --- a/moneymgr_backend/src/controllers/mod.rs +++ b/moneymgr_backend/src/controllers/mod.rs @@ -5,6 +5,7 @@ use std::error::Error; pub mod accounts_controller; pub mod auth_controller; pub mod files_controller; +pub mod movement_controller; pub mod server_controller; pub mod static_controller; pub mod tokens_controller; diff --git a/moneymgr_backend/src/controllers/movement_controller.rs b/moneymgr_backend/src/controllers/movement_controller.rs new file mode 100644 index 0000000..1e90425 --- /dev/null +++ b/moneymgr_backend/src/controllers/movement_controller.rs @@ -0,0 +1,16 @@ +use crate::controllers::HttpResult; +use crate::extractors::auth_extractor::AuthExtractor; +use crate::services::movements_service; +use crate::services::movements_service::UpdateMovementQuery; +use actix_web::{HttpResponse, web}; + +/// Create a new movement +pub async fn create(auth: AuthExtractor, req: web::Json) -> HttpResult { + if let Some(err) = req.check_error(auth.user_id()).await? { + return Ok(HttpResponse::BadRequest().json(err)); + } + + movements_service::create(&req).await?; + + Ok(HttpResponse::Created().finish()) +} diff --git a/moneymgr_backend/src/controllers/server_controller.rs b/moneymgr_backend/src/controllers/server_controller.rs index 32a7cb2..a72984b 100644 --- a/moneymgr_backend/src/controllers/server_controller.rs +++ b/moneymgr_backend/src/controllers/server_controller.rs @@ -41,6 +41,7 @@ pub struct ServerConstraints { pub token_ip_net: LenConstraints, pub token_max_inactivity: LenConstraints, pub account_name: LenConstraints, + pub movement_label: LenConstraints, } impl Default for ServerConstraints { @@ -50,6 +51,7 @@ impl Default for ServerConstraints { token_ip_net: LenConstraints::max_only(44), token_max_inactivity: LenConstraints::new(3600, 3600 * 24 * 365), account_name: LenConstraints::not_empty(50), + movement_label: LenConstraints::not_empty(200), } } } diff --git a/moneymgr_backend/src/main.rs b/moneymgr_backend/src/main.rs index 1cc3964..5aa73d8 100644 --- a/moneymgr_backend/src/main.rs +++ b/moneymgr_backend/src/main.rs @@ -129,6 +129,8 @@ async fn main() -> std::io::Result<()> { "/api/file/{file_id}", web::delete().to(files_controller::delete), ) + // Movement controller + .route("/api/movement", web::post().to(movement_controller::create)) // Static assets .route("/", web::get().to(static_controller::root_index)) .route( diff --git a/moneymgr_backend/src/models/mod.rs b/moneymgr_backend/src/models/mod.rs index f388e95..79b739b 100644 --- a/moneymgr_backend/src/models/mod.rs +++ b/moneymgr_backend/src/models/mod.rs @@ -1,4 +1,5 @@ pub mod accounts; pub mod files; +pub mod movements; pub mod tokens; pub mod users; diff --git a/moneymgr_backend/src/models/movements.rs b/moneymgr_backend/src/models/movements.rs new file mode 100644 index 0000000..ba0a738 --- /dev/null +++ b/moneymgr_backend/src/models/movements.rs @@ -0,0 +1,47 @@ +use crate::models::accounts::AccountID; +use crate::models::files::FileID; +use crate::schema::*; +use diesel::{Insertable, Queryable}; + +#[derive(Debug, Clone, Copy, serde::Serialize, serde::Deserialize, PartialEq, Eq)] +pub struct MovementID(pub i32); + +#[derive(Queryable, Debug, Clone, serde::Serialize)] +pub struct Movement { + id: i32, + account_id: i32, + pub time: i64, + pub label: String, + file_id: Option, + pub amount: f32, + pub checked: bool, + pub time_create: i64, + pub time_update: i64, +} + +impl Movement { + pub fn id(&self) -> MovementID { + MovementID(self.id) + } + + pub fn account_id(&self) -> AccountID { + AccountID(self.account_id) + } + + pub fn file_id(&self) -> Option { + self.file_id.map(FileID) + } +} + +#[derive(Insertable, Debug)] +#[diesel(table_name = movements)] +pub struct NewMovement<'a> { + pub account_id: i32, + pub time: i64, + pub label: &'a str, + pub file_id: Option, + pub amount: f32, + pub checked: bool, + pub time_create: i64, + pub time_update: i64, +} diff --git a/moneymgr_backend/src/services/mod.rs b/moneymgr_backend/src/services/mod.rs index ec1934c..ae31bb5 100644 --- a/moneymgr_backend/src/services/mod.rs +++ b/moneymgr_backend/src/services/mod.rs @@ -1,4 +1,5 @@ pub mod accounts_service; pub mod files_service; +pub mod movements_service; pub mod tokens_service; pub mod users_service; diff --git a/moneymgr_backend/src/services/movements_service.rs b/moneymgr_backend/src/services/movements_service.rs new file mode 100644 index 0000000..f24d940 --- /dev/null +++ b/moneymgr_backend/src/services/movements_service.rs @@ -0,0 +1,89 @@ +use crate::connections::db_connection::db; +use crate::controllers::server_controller::ServerConstraints; +use crate::models::accounts::AccountID; +use crate::models::files::FileID; +use crate::models::movements::{Movement, MovementID, NewMovement}; +use crate::models::users::UserID; +use crate::schema::movements; +use crate::services::{accounts_service, files_service}; +use crate::utils::time_utils::time; +use diesel::RunQueryDsl; +use diesel::prelude::*; + +#[derive(serde::Deserialize)] +pub struct UpdateMovementQuery { + pub account_id: AccountID, + pub time: u64, + pub label: String, + pub file_id: Option, + pub amount: f32, + pub checked: bool, +} + +impl UpdateMovementQuery { + pub async fn check_error(&self, user_id: UserID) -> anyhow::Result> { + let constraints = ServerConstraints::default(); + + // Check movement label + if !constraints.movement_label.check_str(&self.label) { + return Ok(Some("Invalid movement label length!")); + } + + // Check the account + let account = accounts_service::get_by_id(self.account_id).await?; + if account.user_id() != user_id { + return Ok(Some("The user does not own the account!")); + } + + // Check the file + if let Some(file_id) = self.file_id { + let file = files_service::get_file_with_id(file_id)?; + + if file.user_id() != user_id { + return Ok(Some("The user does not own the referenced file!")); + } + } + + // TODO : check for conflict + + Ok(None) + } +} + +/// Create a new movement +pub async fn create(query: &UpdateMovementQuery) -> anyhow::Result { + let new_account = NewMovement { + account_id: query.account_id.0, + time: query.time as i64, + label: &query.label, + file_id: query.file_id.map(|f| f.0), + amount: query.amount, + checked: query.checked, + time_create: time() as i64, + time_update: time() as i64, + }; + + let res: Movement = diesel::insert_into(movements::table) + .values(&new_account) + .get_result(&mut db()?)?; + + update(res.id(), query).await?; + + Ok(res) +} + +/// Update a movement +pub async fn update(id: MovementID, q: &UpdateMovementQuery) -> anyhow::Result<()> { + diesel::update(movements::dsl::movements.filter(movements::dsl::id.eq(id.0))) + .set(( + movements::dsl::time_update.eq(time() as i64), + movements::dsl::account_id.eq(q.account_id.0), + movements::dsl::time.eq(q.time as i64), + movements::dsl::label.eq(&q.label), + movements::dsl::file_id.eq(&q.file_id.map(|f| f.0)), + movements::dsl::amount.eq(q.amount), + )) + .execute(&mut db()?)?; + + Ok(()) +} diff --git a/moneymgr_web/src/api/ServerApi.ts b/moneymgr_web/src/api/ServerApi.ts index dde8ec5..6e74c22 100644 --- a/moneymgr_web/src/api/ServerApi.ts +++ b/moneymgr_web/src/api/ServerApi.ts @@ -17,6 +17,7 @@ export interface ServerConstraints { token_name: LenConstraint; token_ip_net: LenConstraint; token_max_inactivity: LenConstraint; + movement_label: LenConstraint; } export interface LenConstraint {