From b6281be349aa2ad322027778c3949bf50871828e Mon Sep 17 00:00:00 2001 From: Pierre HUBERT Date: Tue, 6 May 2025 23:15:49 +0200 Subject: [PATCH] Add route to create inbox entries --- .../up.sql | 5 +- .../src/controllers/inbox_controller.rs | 16 ++++ moneymgr_backend/src/controllers/mod.rs | 1 + .../src/controllers/server_controller.rs | 2 + moneymgr_backend/src/main.rs | 2 + moneymgr_backend/src/models/inbox.rs | 66 +++++++++++++ moneymgr_backend/src/models/mod.rs | 1 + moneymgr_backend/src/schema.rs | 8 +- .../src/services/inbox_service.rs | 93 +++++++++++++++++++ moneymgr_backend/src/services/mod.rs | 1 + 10 files changed, 192 insertions(+), 3 deletions(-) create mode 100644 moneymgr_backend/src/controllers/inbox_controller.rs create mode 100644 moneymgr_backend/src/models/inbox.rs create mode 100644 moneymgr_backend/src/services/inbox_service.rs diff --git a/moneymgr_backend/migrations/2025-03-17-173101_initial_structure/up.sql b/moneymgr_backend/migrations/2025-03-17-173101_initial_structure/up.sql index 899fc77..19b9c3a 100644 --- a/moneymgr_backend/migrations/2025-03-17-173101_initial_structure/up.sql +++ b/moneymgr_backend/migrations/2025-03-17-173101_initial_structure/up.sql @@ -67,7 +67,10 @@ CREATE TABLE inbox id SERIAL PRIMARY KEY, file_id INTEGER NOT NULL REFERENCES files ON DELETE RESTRICT, user_id INTEGER NOT NULL REFERENCES users ON DELETE CASCADE, - account_id INTEGER REFERENCES accounts ON DELETE CASCADE, + movement_id INTEGER NULL REFERENCES movements ON DELETE CASCADE, + time BIGINT NOT NULL, + label VARCHAR(200) NULL, + amount REAL NULL, time_create BIGINT NOT NULL, time_update BIGINT NOT NULL ); \ No newline at end of file diff --git a/moneymgr_backend/src/controllers/inbox_controller.rs b/moneymgr_backend/src/controllers/inbox_controller.rs new file mode 100644 index 0000000..9f57f26 --- /dev/null +++ b/moneymgr_backend/src/controllers/inbox_controller.rs @@ -0,0 +1,16 @@ +use crate::controllers::HttpResult; +use crate::extractors::auth_extractor::AuthExtractor; +use crate::services::inbox_service; +use crate::services::inbox_service::UpdateInboxEntryQuery; +use actix_web::{HttpResponse, web}; + +/// Create a new inbox entry +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)); + } + + inbox_service::create(auth.user_id(), &req).await?; + + Ok(HttpResponse::Created().finish()) +} diff --git a/moneymgr_backend/src/controllers/mod.rs b/moneymgr_backend/src/controllers/mod.rs index 28e1ca3..e1a0d76 100644 --- a/moneymgr_backend/src/controllers/mod.rs +++ b/moneymgr_backend/src/controllers/mod.rs @@ -8,6 +8,7 @@ pub mod accounts_controller; pub mod auth_controller; pub mod backup_controller; pub mod files_controller; +pub mod inbox_controller; pub mod movement_controller; pub mod server_controller; pub mod static_controller; diff --git a/moneymgr_backend/src/controllers/server_controller.rs b/moneymgr_backend/src/controllers/server_controller.rs index a7e886d..11b3251 100644 --- a/moneymgr_backend/src/controllers/server_controller.rs +++ b/moneymgr_backend/src/controllers/server_controller.rs @@ -42,6 +42,7 @@ pub struct ServerConstraints { pub token_max_inactivity: LenConstraints, pub account_name: LenConstraints, pub movement_label: LenConstraints, + pub inbox_entry_label: LenConstraints, pub file_allowed_types: &'static [&'static str], } @@ -53,6 +54,7 @@ impl Default for ServerConstraints { token_max_inactivity: LenConstraints::new(3600, 3600 * 24 * 365), account_name: LenConstraints::not_empty(50), movement_label: LenConstraints::not_empty(200), + inbox_entry_label: LenConstraints::not_empty(200), file_allowed_types: &[ "image/jpeg", "image/png", diff --git a/moneymgr_backend/src/main.rs b/moneymgr_backend/src/main.rs index bed0b2f..e410943 100644 --- a/moneymgr_backend/src/main.rs +++ b/moneymgr_backend/src/main.rs @@ -155,6 +155,8 @@ async fn main() -> std::io::Result<()> { "/api/movement/{movement_id}", web::delete().to(movement_controller::delete), ) + // Inbox controller + .route("/api/inbox", web::post().to(inbox_controller::create)) // Statistics controller .route("/api/stats/global", web::get().to(stats_controller::global)) .route( diff --git a/moneymgr_backend/src/models/inbox.rs b/moneymgr_backend/src/models/inbox.rs new file mode 100644 index 0000000..e90e462 --- /dev/null +++ b/moneymgr_backend/src/models/inbox.rs @@ -0,0 +1,66 @@ +use crate::models::files::FileID; +use crate::models::movements::MovementID; +use crate::models::users::UserID; +use crate::schema::*; +use diesel::{Insertable, Queryable}; + +#[derive(Debug, Clone, Copy, serde::Serialize, serde::Deserialize, PartialEq, Eq)] +pub struct InboxID(pub i32); + +/// Single inbox entry information +#[derive(Queryable, Debug, Clone, serde::Serialize, serde::Deserialize)] +pub struct Inbox { + /// The ID of the inbox entry + id: i32, + /// ID of the file attached to this inbox entry + file_id: i32, + /// The ID this inbox entry belongs to + user_id: i32, + /// The ID of the movement this inbox entry is attached to (if any) + movement_id: Option, + /// The time this inbox entry happened + pub time: i64, + /// The label associated to this inbox entry + pub label: Option, + /// The amount of the inbox entry + pub amount: Option, + /// The time this inbox entry was created in the database + pub time_create: i64, + /// The time this inbox entry was last updated in the database + pub time_update: i64, +} + +impl Inbox { + /// Get the ID of the inbox entry + pub fn id(&self) -> InboxID { + InboxID(self.id) + } + + /// The id of the user owning this inbox entry + pub fn user_id(&self) -> UserID { + UserID(self.user_id) + } + + /// The ID of the movement attached to the movement (if any) + pub fn movement_id(&self) -> Option { + self.movement_id.map(MovementID) + } + + /// The ID of the file attached to the movement, if any + pub fn file_id(&self) -> FileID { + FileID(self.file_id) + } +} + +#[derive(Insertable, Debug)] +#[diesel(table_name = inbox)] +pub struct NewInboxEntry<'a> { + pub file_id: i32, + pub user_id: i32, + pub movement_id: Option, + pub time: i64, + pub label: Option<&'a str>, + pub amount: Option, + pub time_create: i64, + pub time_update: i64, +} diff --git a/moneymgr_backend/src/models/mod.rs b/moneymgr_backend/src/models/mod.rs index 79b739b..634809a 100644 --- a/moneymgr_backend/src/models/mod.rs +++ b/moneymgr_backend/src/models/mod.rs @@ -1,5 +1,6 @@ pub mod accounts; pub mod files; +pub mod inbox; pub mod movements; pub mod tokens; pub mod users; diff --git a/moneymgr_backend/src/schema.rs b/moneymgr_backend/src/schema.rs index 7507ec2..3c14abf 100644 --- a/moneymgr_backend/src/schema.rs +++ b/moneymgr_backend/src/schema.rs @@ -35,7 +35,11 @@ diesel::table! { id -> Int4, file_id -> Int4, user_id -> Int4, - account_id -> Nullable, + movement_id -> Nullable, + time -> Int8, + #[max_length = 200] + label -> Nullable, + amount -> Nullable, time_create -> Int8, time_update -> Int8, } @@ -94,8 +98,8 @@ diesel::table! { diesel::joinable!(accounts -> users (user_id)); diesel::joinable!(files -> users (user_id)); -diesel::joinable!(inbox -> accounts (account_id)); diesel::joinable!(inbox -> files (file_id)); +diesel::joinable!(inbox -> movements (movement_id)); diesel::joinable!(inbox -> users (user_id)); diesel::joinable!(movements -> accounts (account_id)); diesel::joinable!(movements -> files (file_id)); diff --git a/moneymgr_backend/src/services/inbox_service.rs b/moneymgr_backend/src/services/inbox_service.rs new file mode 100644 index 0000000..f186be9 --- /dev/null +++ b/moneymgr_backend/src/services/inbox_service.rs @@ -0,0 +1,93 @@ +use crate::connections::db_connection::db; +use crate::controllers::server_controller::ServerConstraints; +use crate::models::files::FileID; +use crate::models::inbox::{Inbox, InboxID, NewInboxEntry}; +use crate::models::movements::MovementID; +use crate::models::users::UserID; +use crate::schema::inbox; +use crate::services::{accounts_service, files_service, movements_service}; +use crate::utils::time_utils::time; +use diesel::prelude::*; + +#[derive(serde::Deserialize)] +pub struct UpdateInboxEntryQuery { + pub file_id: FileID, + pub movement_id: Option, + pub time: u64, + pub label: Option, + pub amount: Option, +} + +impl UpdateInboxEntryQuery { + pub async fn check_error(&self, user_id: UserID) -> anyhow::Result> { + let constraints = ServerConstraints::default(); + + // Check inbox entry label + if let Some(label) = &self.label { + if !constraints.inbox_entry_label.check_str(label) { + return Ok(Some("Invalid inbox entry label length!")); + } + } + + // Check the referenced movement + if let Some(movement_id) = self.movement_id { + let Ok(movement) = movements_service::get_by_id(movement_id).await else { + return Ok(Some("Movement not found")); + }; + + let account = accounts_service::get_by_id(movement.account_id()).await?; + + if account.user_id() != user_id { + return Ok(Some("The account does not own this movement!")); + } + } + + // Check the file + let Ok(file) = files_service::get_file_with_id(self.file_id) else { + return Ok(Some("The account does not own this file!")); + }; + if file.user_id() != user_id { + return Ok(Some("The user does not own the referenced file!")); + } + + Ok(None) + } +} + +/// Create a new inbox entry +pub async fn create(user_id: UserID, query: &UpdateInboxEntryQuery) -> anyhow::Result { + let new_entry = NewInboxEntry { + time: query.time as i64, + label: query.label.as_deref(), + file_id: query.file_id.0, + user_id: user_id.0, + amount: query.amount, + time_create: time() as i64, + time_update: time() as i64, + movement_id: None, + }; + + let res: Inbox = diesel::insert_into(inbox::table) + .values(&new_entry) + .get_result(&mut db()?)?; + + update(res.id(), query).await?; + + Ok(res) +} + +/// Update a inbox entry +pub async fn update(id: InboxID, q: &UpdateInboxEntryQuery) -> anyhow::Result<()> { + diesel::update(inbox::dsl::inbox.filter(inbox::dsl::id.eq(id.0))) + .set(( + inbox::dsl::time_update.eq(time() as i64), + inbox::dsl::movement_id.eq(q.movement_id.map(|m| m.0)), + inbox::dsl::time.eq(q.time as i64), + inbox::dsl::label.eq(&q.label), + inbox::dsl::file_id.eq(&q.file_id.0), + inbox::dsl::amount.eq(q.amount), + )) + .execute(&mut db()?)?; + + Ok(()) +} diff --git a/moneymgr_backend/src/services/mod.rs b/moneymgr_backend/src/services/mod.rs index ae31bb5..3544a5c 100644 --- a/moneymgr_backend/src/services/mod.rs +++ b/moneymgr_backend/src/services/mod.rs @@ -1,5 +1,6 @@ pub mod accounts_service; pub mod files_service; +pub mod inbox_service; pub mod movements_service; pub mod tokens_service; pub mod users_service;