feat(backend): scope files to users with FK user_id, rename storage_path to storage_key for clarity
This commit is contained in:
@@ -0,0 +1 @@
|
|||||||
|
ALTER TABLE file_records ADD COLUMN user_id BIGINT NOT NULL REFERENCES users(id);
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
ALTER TABLE file_records RENAME COLUMN storage_path TO storage_key;
|
||||||
@@ -12,12 +12,12 @@ use crate::error::LoftError;
|
|||||||
#[serde(rename_all = "camelCase")]
|
#[serde(rename_all = "camelCase")]
|
||||||
pub struct FileRecord {
|
pub struct FileRecord {
|
||||||
pub id: i64,
|
pub id: i64,
|
||||||
// pub user_id: i64,
|
pub user_id: i64,
|
||||||
pub name: String,
|
pub name: String,
|
||||||
pub file_type: String,
|
pub file_type: String,
|
||||||
pub size: i64,
|
pub size: i64,
|
||||||
#[serde(skip_serializing)]
|
#[serde(skip_serializing)]
|
||||||
pub storage_path: String,
|
pub storage_key: String,
|
||||||
pub uploaded_at: chrono::DateTime<chrono::Utc>,
|
pub uploaded_at: chrono::DateTime<chrono::Utc>,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -75,6 +75,7 @@ impl FileRepository {
|
|||||||
|
|
||||||
pub async fn create_file_record(
|
pub async fn create_file_record(
|
||||||
&self,
|
&self,
|
||||||
|
user_id: i64,
|
||||||
file_name: &str,
|
file_name: &str,
|
||||||
file_size: usize,
|
file_size: usize,
|
||||||
file_storage_key: &str,
|
file_storage_key: &str,
|
||||||
@@ -83,10 +84,11 @@ impl FileRepository {
|
|||||||
let file_record = sqlx::query_as!(
|
let file_record = sqlx::query_as!(
|
||||||
FileRecord,
|
FileRecord,
|
||||||
r#"
|
r#"
|
||||||
INSERT INTO file_records (name, file_type, size, storage_path)
|
INSERT INTO file_records (user_id, name, file_type, size, storage_key)
|
||||||
VALUES ($1, $2, $3, $4)
|
VALUES ($1, $2, $3, $4, $5)
|
||||||
RETURNING *
|
RETURNING *
|
||||||
"#,
|
"#,
|
||||||
|
user_id,
|
||||||
file_name,
|
file_name,
|
||||||
"TODO-file_type".to_string(),
|
"TODO-file_type".to_string(),
|
||||||
file_size as i64,
|
file_size as i64,
|
||||||
@@ -109,7 +111,7 @@ impl FileRepository {
|
|||||||
);
|
);
|
||||||
let record = self.get_file(file_id).await?;
|
let record = self.get_file(file_id).await?;
|
||||||
info!("Downloading file \"{}\"", file_id);
|
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();
|
let stream = reader.into_bytes_stream(0..).await.unwrap();
|
||||||
|
|
||||||
Ok(stream)
|
Ok(stream)
|
||||||
@@ -144,7 +146,7 @@ impl FileRepository {
|
|||||||
);
|
);
|
||||||
let record = self.get_file(file_id).await?;
|
let record = self.get_file(file_id).await?;
|
||||||
info!("Deleting file bytes \"{}\"", file_id);
|
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);
|
info!("Deleting file record \"{}\"", file_id);
|
||||||
sqlx::query_as!(
|
sqlx::query_as!(
|
||||||
@@ -162,13 +164,15 @@ impl FileRepository {
|
|||||||
.ok_or(LoftError::FileIdNotFound)
|
.ok_or(LoftError::FileIdNotFound)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn list_files(&self) -> Result<Vec<FileRecord>, LoftError> {
|
pub async fn list_files(&self, user_id: i64) -> Result<Vec<FileRecord>, LoftError> {
|
||||||
let files = sqlx::query_as!(
|
let files = sqlx::query_as!(
|
||||||
FileRecord,
|
FileRecord,
|
||||||
r#"
|
r#"
|
||||||
SELECT *
|
SELECT *
|
||||||
FROM file_records fr
|
FROM file_records fr
|
||||||
"#
|
WHERE user_id = $1
|
||||||
|
"#,
|
||||||
|
user_id
|
||||||
)
|
)
|
||||||
.fetch_all(&self.pool)
|
.fetch_all(&self.pool)
|
||||||
.await
|
.await
|
||||||
@@ -333,6 +337,9 @@ mod tests {
|
|||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
#[serial_test::serial]
|
#[serial_test::serial]
|
||||||
async fn test_upload_and_list() {
|
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();
|
let file_repository = file_repository().await.unwrap();
|
||||||
truncate_file_records(&file_repository.pool).await;
|
truncate_file_records(&file_repository.pool).await;
|
||||||
|
|
||||||
@@ -352,22 +359,25 @@ mod tests {
|
|||||||
.unwrap();
|
.unwrap();
|
||||||
|
|
||||||
file_repository
|
file_repository
|
||||||
.create_file_record("a.png", 2, "a.jpg-uuid")
|
.create_file_record(user1.id, "a.png", 2, "a.jpg-uuid")
|
||||||
.await
|
.await
|
||||||
.unwrap();
|
.unwrap();
|
||||||
file_repository
|
file_repository
|
||||||
.create_file_record("b.png", 2, "b.jpg-uuid")
|
.create_file_record(user2.id, "b.png", 2, "b.jpg-uuid")
|
||||||
.await
|
.await
|
||||||
.unwrap();
|
.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_file_records(&file_repository.pool).await;
|
||||||
|
truncate_users(&user_repository.pool).await;
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
#[serial_test::serial]
|
#[serial_test::serial]
|
||||||
async fn test_download() {
|
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();
|
let file_repository = file_repository().await.unwrap();
|
||||||
truncate_file_records(&file_repository.pool).await;
|
truncate_file_records(&file_repository.pool).await;
|
||||||
|
|
||||||
@@ -382,14 +392,16 @@ mod tests {
|
|||||||
.unwrap();
|
.unwrap();
|
||||||
|
|
||||||
let file_record = file_repository
|
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
|
.await
|
||||||
.unwrap();
|
.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_file_records(&file_repository.pool).await;
|
||||||
|
truncate_users(&user_repository.pool).await;
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
@@ -407,6 +419,8 @@ mod tests {
|
|||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
#[serial_test::serial]
|
#[serial_test::serial]
|
||||||
async fn test_delete() {
|
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();
|
let file_repository = file_repository().await.unwrap();
|
||||||
truncate_file_records(&file_repository.pool).await;
|
truncate_file_records(&file_repository.pool).await;
|
||||||
|
|
||||||
@@ -421,7 +435,7 @@ mod tests {
|
|||||||
.unwrap();
|
.unwrap();
|
||||||
|
|
||||||
let file_record = file_repository
|
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
|
.await
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
|
||||||
@@ -432,6 +446,7 @@ mod tests {
|
|||||||
Err(LoftError::FileIdNotFound)
|
Err(LoftError::FileIdNotFound)
|
||||||
));
|
));
|
||||||
truncate_file_records(&file_repository.pool).await;
|
truncate_file_records(&file_repository.pool).await;
|
||||||
|
truncate_users(&user_repository.pool).await;
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
|
|||||||
@@ -9,8 +9,7 @@ use sqlx::types::uuid;
|
|||||||
use tracing::info;
|
use tracing::info;
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
error::LoftError,
|
ctx::Ctx, error::LoftError, model::{FileRecord, FileRepository}
|
||||||
model::{FileRecord, FileRepository},
|
|
||||||
};
|
};
|
||||||
|
|
||||||
pub fn routes_file(file_repository: FileRepository) -> Router {
|
pub fn routes_file(file_repository: FileRepository) -> Router {
|
||||||
@@ -23,6 +22,7 @@ pub fn routes_file(file_repository: FileRepository) -> Router {
|
|||||||
|
|
||||||
async fn upload_file(
|
async fn upload_file(
|
||||||
State(file_repository): State<FileRepository>,
|
State(file_repository): State<FileRepository>,
|
||||||
|
ctx: Ctx,
|
||||||
mut multipart: Multipart,
|
mut multipart: Multipart,
|
||||||
) -> Result<Json<FileRecord>, LoftError> {
|
) -> Result<Json<FileRecord>, LoftError> {
|
||||||
info!("handler: upload_file");
|
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) {
|
if let (Some(name), Some(key), Some(size)) = (file_name, file_storage_key, file_size) {
|
||||||
let file_record = file_repository
|
let file_record = file_repository
|
||||||
.create_file_record(&name, size, &key)
|
.create_file_record(ctx.user_id(), &name, size, &key)
|
||||||
.await?;
|
.await?;
|
||||||
return Ok(Json(file_record));
|
return Ok(Json(file_record));
|
||||||
}
|
}
|
||||||
@@ -82,11 +82,11 @@ async fn delete_file(
|
|||||||
|
|
||||||
async fn list_files(
|
async fn list_files(
|
||||||
State(file_repository): State<FileRepository>,
|
State(file_repository): State<FileRepository>,
|
||||||
// can add a filters param here
|
ctx: Ctx
|
||||||
) -> Result<Json<Vec<FileRecord>>, LoftError> {
|
) -> Result<Json<Vec<FileRecord>>, LoftError> {
|
||||||
info!("handler: list_files");
|
info!("handler: list_files");
|
||||||
|
|
||||||
let files = file_repository.list_files().await?;
|
let files = file_repository.list_files(ctx.user_id()).await?;
|
||||||
Ok(Json(files))
|
Ok(Json(files))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -5,7 +5,6 @@ use argon2::{
|
|||||||
use axum::{Json, Router, extract::State, http::StatusCode, routing::post};
|
use axum::{Json, Router, extract::State, http::StatusCode, routing::post};
|
||||||
use rand::RngExt;
|
use rand::RngExt;
|
||||||
use serde::Deserialize;
|
use serde::Deserialize;
|
||||||
use sqlx::PgPool;
|
|
||||||
use tower_cookies::{Cookie, Cookies};
|
use tower_cookies::{Cookie, Cookies};
|
||||||
|
|
||||||
use crate::{error::LoftError, model::UserRepository, web::AUTH_TOKEN};
|
use crate::{error::LoftError, model::UserRepository, web::AUTH_TOKEN};
|
||||||
@@ -153,6 +152,9 @@ mod tests {
|
|||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
#[serial_test::serial]
|
#[serial_test::serial]
|
||||||
async fn test_routes_login_wrong_credentials() {
|
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 server = test_server().await;
|
||||||
let response = server
|
let response = server
|
||||||
.post("/api/auth/login")
|
.post("/api/auth/login")
|
||||||
@@ -162,6 +164,8 @@ mod tests {
|
|||||||
}))
|
}))
|
||||||
.await;
|
.await;
|
||||||
response.assert_status_unauthorized();
|
response.assert_status_unauthorized();
|
||||||
|
truncate_sessions(&user_repository.pool).await;
|
||||||
|
truncate_users(&user_repository.pool).await;
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
@@ -186,6 +190,8 @@ mod tests {
|
|||||||
.await;
|
.await;
|
||||||
|
|
||||||
response.assert_status_ok();
|
response.assert_status_ok();
|
||||||
|
truncate_sessions(&user_repository.pool).await;
|
||||||
|
truncate_users(&user_repository.pool).await;
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
@@ -218,6 +224,8 @@ mod tests {
|
|||||||
let logout_response = server.post("/api/auth/logout").add_cookie(cookie).await;
|
let logout_response = server.post("/api/auth/logout").add_cookie(cookie).await;
|
||||||
|
|
||||||
logout_response.assert_status_ok();
|
logout_response.assert_status_ok();
|
||||||
|
truncate_sessions(&user_repository.pool).await;
|
||||||
|
truncate_users(&user_repository.pool).await;
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
@@ -234,5 +242,7 @@ mod tests {
|
|||||||
.await;
|
.await;
|
||||||
|
|
||||||
register_response.assert_status(StatusCode::CREATED);
|
register_response.assert_status(StatusCode::CREATED);
|
||||||
|
truncate_sessions(&user_repository.pool).await;
|
||||||
|
truncate_users(&user_repository.pool).await;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user