Add route to create inbox entries
This commit is contained in:
		@@ -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
 | 
			
		||||
);
 | 
			
		||||
							
								
								
									
										16
									
								
								moneymgr_backend/src/controllers/inbox_controller.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										16
									
								
								moneymgr_backend/src/controllers/inbox_controller.rs
									
									
									
									
									
										Normal file
									
								
							@@ -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<UpdateInboxEntryQuery>) -> 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())
 | 
			
		||||
}
 | 
			
		||||
@@ -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;
 | 
			
		||||
 
 | 
			
		||||
@@ -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",
 | 
			
		||||
 
 | 
			
		||||
@@ -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(
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										66
									
								
								moneymgr_backend/src/models/inbox.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										66
									
								
								moneymgr_backend/src/models/inbox.rs
									
									
									
									
									
										Normal file
									
								
							@@ -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<i32>,
 | 
			
		||||
    /// The time this inbox entry happened
 | 
			
		||||
    pub time: i64,
 | 
			
		||||
    /// The label associated to this inbox entry
 | 
			
		||||
    pub label: Option<String>,
 | 
			
		||||
    /// The amount of the inbox entry
 | 
			
		||||
    pub amount: Option<f32>,
 | 
			
		||||
    /// 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<MovementID> {
 | 
			
		||||
        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<i32>,
 | 
			
		||||
    pub time: i64,
 | 
			
		||||
    pub label: Option<&'a str>,
 | 
			
		||||
    pub amount: Option<f32>,
 | 
			
		||||
    pub time_create: i64,
 | 
			
		||||
    pub time_update: i64,
 | 
			
		||||
}
 | 
			
		||||
@@ -1,5 +1,6 @@
 | 
			
		||||
pub mod accounts;
 | 
			
		||||
pub mod files;
 | 
			
		||||
pub mod inbox;
 | 
			
		||||
pub mod movements;
 | 
			
		||||
pub mod tokens;
 | 
			
		||||
pub mod users;
 | 
			
		||||
 
 | 
			
		||||
@@ -35,7 +35,11 @@ diesel::table! {
 | 
			
		||||
        id -> Int4,
 | 
			
		||||
        file_id -> Int4,
 | 
			
		||||
        user_id -> Int4,
 | 
			
		||||
        account_id -> Nullable<Int4>,
 | 
			
		||||
        movement_id -> Nullable<Int4>,
 | 
			
		||||
        time -> Int8,
 | 
			
		||||
        #[max_length = 200]
 | 
			
		||||
        label -> Nullable<Varchar>,
 | 
			
		||||
        amount -> Nullable<Float4>,
 | 
			
		||||
        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));
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										93
									
								
								moneymgr_backend/src/services/inbox_service.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										93
									
								
								moneymgr_backend/src/services/inbox_service.rs
									
									
									
									
									
										Normal file
									
								
							@@ -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<MovementID>,
 | 
			
		||||
    pub time: u64,
 | 
			
		||||
    pub label: Option<String>,
 | 
			
		||||
    pub amount: Option<f32>,
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
impl UpdateInboxEntryQuery {
 | 
			
		||||
    pub async fn check_error(&self, user_id: UserID) -> anyhow::Result<Option<&'static str>> {
 | 
			
		||||
        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<Inbox> {
 | 
			
		||||
    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(())
 | 
			
		||||
}
 | 
			
		||||
@@ -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;
 | 
			
		||||
 
 | 
			
		||||
		Reference in New Issue
	
	Block a user