From 2501f6ca67ffb2202e4922398c2c9e8beefee1c7 Mon Sep 17 00:00:00 2001 From: stefiosif Date: Thu, 28 May 2026 21:24:12 +0300 Subject: [PATCH] feat(backend): scope files to users with FK user_id, rename storage_path to storage_key for clarity --- ...0528174135_add_user_id_to_file_records.sql | 1 + ...203_rename_storage_path_to_storage_key.sql | 1 + backend/src/model.rs | 47 ++++++++++++------- backend/src/web/routes_file.rs | 10 ++-- backend/src/web/routes_login.rs | 16 +++++-- 5 files changed, 51 insertions(+), 24 deletions(-) create mode 100644 backend/migrations/20260528174135_add_user_id_to_file_records.sql create mode 100644 backend/migrations/20260528174203_rename_storage_path_to_storage_key.sql diff --git a/backend/migrations/20260528174135_add_user_id_to_file_records.sql b/backend/migrations/20260528174135_add_user_id_to_file_records.sql new file mode 100644 index 0000000..8a4c850 --- /dev/null +++ b/backend/migrations/20260528174135_add_user_id_to_file_records.sql @@ -0,0 +1 @@ +ALTER TABLE file_records ADD COLUMN user_id BIGINT NOT NULL REFERENCES users(id); diff --git a/backend/migrations/20260528174203_rename_storage_path_to_storage_key.sql b/backend/migrations/20260528174203_rename_storage_path_to_storage_key.sql new file mode 100644 index 0000000..bd92978 --- /dev/null +++ b/backend/migrations/20260528174203_rename_storage_path_to_storage_key.sql @@ -0,0 +1 @@ +ALTER TABLE file_records RENAME COLUMN storage_path TO storage_key; diff --git a/backend/src/model.rs b/backend/src/model.rs index 965f806..ccbc64c 100644 --- a/backend/src/model.rs +++ b/backend/src/model.rs @@ -12,12 +12,12 @@ use crate::error::LoftError; #[serde(rename_all = "camelCase")] pub struct FileRecord { pub id: i64, - // pub user_id: i64, + pub user_id: i64, pub name: String, pub file_type: String, pub size: i64, #[serde(skip_serializing)] - pub storage_path: String, + pub storage_key: String, pub uploaded_at: chrono::DateTime, } @@ -75,6 +75,7 @@ impl FileRepository { pub async fn create_file_record( &self, + user_id: i64, file_name: &str, file_size: usize, file_storage_key: &str, @@ -83,10 +84,11 @@ impl FileRepository { let file_record = sqlx::query_as!( FileRecord, r#" - INSERT INTO file_records (name, file_type, size, storage_path) - VALUES ($1, $2, $3, $4) + INSERT INTO file_records (user_id, name, file_type, size, storage_key) + VALUES ($1, $2, $3, $4, $5) RETURNING * "#, + user_id, file_name, "TODO-file_type".to_string(), file_size as i64, @@ -109,7 +111,7 @@ impl FileRepository { ); let record = self.get_file(file_id).await?; info!("Downloading file \"{}\"", file_id); - let reader = self.op.reader(&record.storage_path).await.unwrap(); + let reader = self.op.reader(&record.storage_key).await.unwrap(); let stream = reader.into_bytes_stream(0..).await.unwrap(); Ok(stream) @@ -144,7 +146,7 @@ impl FileRepository { ); let record = self.get_file(file_id).await?; info!("Deleting file bytes \"{}\"", file_id); - self.op.delete(&record.storage_path).await.unwrap(); + self.op.delete(&record.storage_key).await.unwrap(); info!("Deleting file record \"{}\"", file_id); sqlx::query_as!( @@ -162,13 +164,15 @@ impl FileRepository { .ok_or(LoftError::FileIdNotFound) } - pub async fn list_files(&self) -> Result, LoftError> { + pub async fn list_files(&self, user_id: i64) -> Result, LoftError> { let files = sqlx::query_as!( FileRecord, r#" SELECT * FROM file_records fr - "# + WHERE user_id = $1 + "#, + user_id ) .fetch_all(&self.pool) .await @@ -333,6 +337,9 @@ mod tests { #[tokio::test] #[serial_test::serial] async fn test_upload_and_list() { + let user_repository = user_repository().await.unwrap(); + let user1 = user_repository.create_user("username1".to_string(), "password_hash".to_string()).await.unwrap(); + let user2 = user_repository.create_user("username2".to_string(), "password_hash".to_string()).await.unwrap(); let file_repository = file_repository().await.unwrap(); truncate_file_records(&file_repository.pool).await; @@ -352,22 +359,25 @@ mod tests { .unwrap(); file_repository - .create_file_record("a.png", 2, "a.jpg-uuid") + .create_file_record(user1.id, "a.png", 2, "a.jpg-uuid") .await .unwrap(); file_repository - .create_file_record("b.png", 2, "b.jpg-uuid") + .create_file_record(user2.id, "b.png", 2, "b.jpg-uuid") .await .unwrap(); - let files = file_repository.list_files().await.unwrap(); + let files = file_repository.list_files(user1.id).await.unwrap(); - assert_eq!(files.len(), 2); + assert_eq!(files.len(), 1); truncate_file_records(&file_repository.pool).await; + truncate_users(&user_repository.pool).await; } #[tokio::test] #[serial_test::serial] async fn test_download() { + let user_repository = user_repository().await.unwrap(); + let user = user_repository.create_user("username".to_string(), "password_hash".to_string()).await.unwrap(); let file_repository = file_repository().await.unwrap(); truncate_file_records(&file_repository.pool).await; @@ -382,14 +392,16 @@ mod tests { .unwrap(); let file_record = file_repository - .create_file_record("a.jpg", file_size, &file_storage_key) + .create_file_record(user.id, "a.jpg", file_size, &file_storage_key) .await .unwrap(); - let downloaded = file_repository.download_file(file_record.id).await.unwrap(); + let _downloaded = file_repository.download_file(file_record.id).await.unwrap(); + + //TODO: add assert - // assert!(); truncate_file_records(&file_repository.pool).await; + truncate_users(&user_repository.pool).await; } #[tokio::test] @@ -407,6 +419,8 @@ mod tests { #[tokio::test] #[serial_test::serial] async fn test_delete() { + let user_repository = user_repository().await.unwrap(); + let user = user_repository.create_user("username".to_string(), "password_hash".to_string()).await.unwrap(); let file_repository = file_repository().await.unwrap(); truncate_file_records(&file_repository.pool).await; @@ -421,7 +435,7 @@ mod tests { .unwrap(); let file_record = file_repository - .create_file_record("a.jpg", file_size, &file_storage_key) + .create_file_record(user.id, "a.jpg", file_size, &file_storage_key) .await .unwrap(); @@ -432,6 +446,7 @@ mod tests { Err(LoftError::FileIdNotFound) )); truncate_file_records(&file_repository.pool).await; + truncate_users(&user_repository.pool).await; } #[tokio::test] diff --git a/backend/src/web/routes_file.rs b/backend/src/web/routes_file.rs index 906f524..d0f57e9 100644 --- a/backend/src/web/routes_file.rs +++ b/backend/src/web/routes_file.rs @@ -9,8 +9,7 @@ use sqlx::types::uuid; use tracing::info; use crate::{ - error::LoftError, - model::{FileRecord, FileRepository}, + ctx::Ctx, error::LoftError, model::{FileRecord, FileRepository} }; pub fn routes_file(file_repository: FileRepository) -> Router { @@ -23,6 +22,7 @@ pub fn routes_file(file_repository: FileRepository) -> Router { async fn upload_file( State(file_repository): State, + ctx: Ctx, mut multipart: Multipart, ) -> Result, LoftError> { info!("handler: upload_file"); @@ -44,7 +44,7 @@ async fn upload_file( if let (Some(name), Some(key), Some(size)) = (file_name, file_storage_key, file_size) { let file_record = file_repository - .create_file_record(&name, size, &key) + .create_file_record(ctx.user_id(), &name, size, &key) .await?; return Ok(Json(file_record)); } @@ -82,11 +82,11 @@ async fn delete_file( async fn list_files( State(file_repository): State, - // can add a filters param here + ctx: Ctx ) -> Result>, LoftError> { info!("handler: list_files"); - let files = file_repository.list_files().await?; + let files = file_repository.list_files(ctx.user_id()).await?; Ok(Json(files)) } diff --git a/backend/src/web/routes_login.rs b/backend/src/web/routes_login.rs index 50e6b32..de8c957 100644 --- a/backend/src/web/routes_login.rs +++ b/backend/src/web/routes_login.rs @@ -5,11 +5,10 @@ use argon2::{ use axum::{Json, Router, extract::State, http::StatusCode, routing::post}; use rand::RngExt; use serde::Deserialize; -use sqlx::PgPool; use tower_cookies::{Cookie, Cookies}; use crate::{error::LoftError, model::UserRepository, web::AUTH_TOKEN}; - + pub fn routes_auth(user_repository: UserRepository) -> Router { Router::new() .route("/login", post(login)) @@ -118,7 +117,7 @@ mod tests { web::{AUTH_TOKEN, routes_login::routes_auth}, }; - async fn truncate_users(pool: &PgPool) { + async fn truncate_users(pool: &PgPool) { sqlx::query!("TRUNCATE TABLE users CASCADE") .execute(pool) .await @@ -153,6 +152,9 @@ mod tests { #[tokio::test] #[serial_test::serial] async fn test_routes_login_wrong_credentials() { + let user_repository = user_repository().await; + truncate_sessions(&user_repository.pool).await; + truncate_users(&user_repository.pool).await; let server = test_server().await; let response = server .post("/api/auth/login") @@ -162,6 +164,8 @@ mod tests { })) .await; response.assert_status_unauthorized(); + truncate_sessions(&user_repository.pool).await; + truncate_users(&user_repository.pool).await; } #[tokio::test] @@ -186,6 +190,8 @@ mod tests { .await; response.assert_status_ok(); + truncate_sessions(&user_repository.pool).await; + truncate_users(&user_repository.pool).await; } #[tokio::test] @@ -218,6 +224,8 @@ mod tests { let logout_response = server.post("/api/auth/logout").add_cookie(cookie).await; logout_response.assert_status_ok(); + truncate_sessions(&user_repository.pool).await; + truncate_users(&user_repository.pool).await; } #[tokio::test] @@ -234,5 +242,7 @@ mod tests { .await; register_response.assert_status(StatusCode::CREATED); + truncate_sessions(&user_repository.pool).await; + truncate_users(&user_repository.pool).await; } }