Add route to create inbox entries

This commit is contained in:
Pierre HUBERT 2025-05-06 23:15:49 +02:00
parent 68bc15cccc
commit b6281be349
10 changed files with 192 additions and 3 deletions

View File

@ -67,7 +67,10 @@ CREATE TABLE inbox
id SERIAL PRIMARY KEY, id SERIAL PRIMARY KEY,
file_id INTEGER NOT NULL REFERENCES files ON DELETE RESTRICT, file_id INTEGER NOT NULL REFERENCES files ON DELETE RESTRICT,
user_id INTEGER NOT NULL REFERENCES users ON DELETE CASCADE, 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_create BIGINT NOT NULL,
time_update BIGINT NOT NULL time_update BIGINT NOT NULL
); );

View 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())
}

View File

@ -8,6 +8,7 @@ pub mod accounts_controller;
pub mod auth_controller; pub mod auth_controller;
pub mod backup_controller; pub mod backup_controller;
pub mod files_controller; pub mod files_controller;
pub mod inbox_controller;
pub mod movement_controller; pub mod movement_controller;
pub mod server_controller; pub mod server_controller;
pub mod static_controller; pub mod static_controller;

View File

@ -42,6 +42,7 @@ pub struct ServerConstraints {
pub token_max_inactivity: LenConstraints, pub token_max_inactivity: LenConstraints,
pub account_name: LenConstraints, pub account_name: LenConstraints,
pub movement_label: LenConstraints, pub movement_label: LenConstraints,
pub inbox_entry_label: LenConstraints,
pub file_allowed_types: &'static [&'static str], pub file_allowed_types: &'static [&'static str],
} }
@ -53,6 +54,7 @@ impl Default for ServerConstraints {
token_max_inactivity: LenConstraints::new(3600, 3600 * 24 * 365), token_max_inactivity: LenConstraints::new(3600, 3600 * 24 * 365),
account_name: LenConstraints::not_empty(50), account_name: LenConstraints::not_empty(50),
movement_label: LenConstraints::not_empty(200), movement_label: LenConstraints::not_empty(200),
inbox_entry_label: LenConstraints::not_empty(200),
file_allowed_types: &[ file_allowed_types: &[
"image/jpeg", "image/jpeg",
"image/png", "image/png",

View File

@ -155,6 +155,8 @@ async fn main() -> std::io::Result<()> {
"/api/movement/{movement_id}", "/api/movement/{movement_id}",
web::delete().to(movement_controller::delete), web::delete().to(movement_controller::delete),
) )
// Inbox controller
.route("/api/inbox", web::post().to(inbox_controller::create))
// Statistics controller // Statistics controller
.route("/api/stats/global", web::get().to(stats_controller::global)) .route("/api/stats/global", web::get().to(stats_controller::global))
.route( .route(

View 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,
}

View File

@ -1,5 +1,6 @@
pub mod accounts; pub mod accounts;
pub mod files; pub mod files;
pub mod inbox;
pub mod movements; pub mod movements;
pub mod tokens; pub mod tokens;
pub mod users; pub mod users;

View File

@ -35,7 +35,11 @@ diesel::table! {
id -> Int4, id -> Int4,
file_id -> Int4, file_id -> Int4,
user_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_create -> Int8,
time_update -> Int8, time_update -> Int8,
} }
@ -94,8 +98,8 @@ diesel::table! {
diesel::joinable!(accounts -> users (user_id)); diesel::joinable!(accounts -> users (user_id));
diesel::joinable!(files -> users (user_id)); diesel::joinable!(files -> users (user_id));
diesel::joinable!(inbox -> accounts (account_id));
diesel::joinable!(inbox -> files (file_id)); diesel::joinable!(inbox -> files (file_id));
diesel::joinable!(inbox -> movements (movement_id));
diesel::joinable!(inbox -> users (user_id)); diesel::joinable!(inbox -> users (user_id));
diesel::joinable!(movements -> accounts (account_id)); diesel::joinable!(movements -> accounts (account_id));
diesel::joinable!(movements -> files (file_id)); diesel::joinable!(movements -> files (file_id));

View 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(())
}

View File

@ -1,5 +1,6 @@
pub mod accounts_service; pub mod accounts_service;
pub mod files_service; pub mod files_service;
pub mod inbox_service;
pub mod movements_service; pub mod movements_service;
pub mod tokens_service; pub mod tokens_service;
pub mod users_service; pub mod users_service;