fix(security): patch 3 vulnerabilities — IDOR, ownership bypass, XSS
V1: Add owner-scoped folder pagination (list_folders_by_owner_paginated) - New method in FolderRepository trait, PG implementation, service & handler - Prevents IDOR by filtering folder listings to authenticated user V2: Enforce ownership checks on folder mutations - rename_folder, move_folder, delete_folder now require caller_id - Service verifies folder.owner_id == caller_id (returns 404 on mismatch) - Propagated to folder_handler, batch_handler, batch_operations, webdav_handler - delete_folder_with_trash upgraded from OptionalAuthUser to AuthUser - download_folder_zip now checks ownership before streaming V3: Fix XSS in frontend via DOM APIs - sharedView.js: innerHTML → createElement + textContent - contextMenus.js: innerHTML → DOM construction for share dialog Cleanup: removed unused OptionalAuthUser import, updated all stubs/mocks
This commit is contained in:
@@ -38,15 +38,23 @@ pub trait FolderUseCase: Send + Sync + 'static {
|
||||
pagination: &crate::application::dtos::pagination::PaginationRequestDto,
|
||||
) -> Result<crate::application::dtos::pagination::PaginatedResponseDto<FolderDto>, DomainError>;
|
||||
|
||||
/// Renames a folder
|
||||
async fn rename_folder(&self, id: &str, dto: RenameFolderDto)
|
||||
/// Lists folders with pagination, scoped to a specific owner.
|
||||
async fn list_folders_for_owner_paginated(
|
||||
&self,
|
||||
parent_id: Option<&str>,
|
||||
owner_id: &str,
|
||||
pagination: &crate::application::dtos::pagination::PaginationRequestDto,
|
||||
) -> Result<crate::application::dtos::pagination::PaginatedResponseDto<FolderDto>, DomainError>;
|
||||
|
||||
/// Renames a folder (ownership verified against caller_id)
|
||||
async fn rename_folder(&self, id: &str, dto: RenameFolderDto, caller_id: &str)
|
||||
-> Result<FolderDto, DomainError>;
|
||||
|
||||
/// Moves a folder to another parent
|
||||
async fn move_folder(&self, id: &str, dto: MoveFolderDto) -> Result<FolderDto, DomainError>;
|
||||
/// Moves a folder to another parent (ownership verified against caller_id)
|
||||
async fn move_folder(&self, id: &str, dto: MoveFolderDto, caller_id: &str) -> Result<FolderDto, DomainError>;
|
||||
|
||||
/// Deletes a folder
|
||||
async fn delete_folder(&self, id: &str) -> Result<(), DomainError>;
|
||||
/// Deletes a folder (ownership verified against caller_id)
|
||||
async fn delete_folder(&self, id: &str, caller_id: &str) -> Result<(), DomainError>;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -408,6 +408,7 @@ impl BatchOperationService {
|
||||
&self,
|
||||
folder_ids: Vec<String>,
|
||||
_recursive: bool,
|
||||
caller_id: &str,
|
||||
) -> Result<BatchResult<String>, BatchOperationError> {
|
||||
info!("Starting batch deletion of {} folders", folder_ids.len());
|
||||
let start_time = std::time::Instant::now();
|
||||
@@ -427,14 +428,13 @@ impl BatchOperationService {
|
||||
let folder_service = self.folder_service.clone();
|
||||
let semaphore = self.semaphore.clone();
|
||||
let id_clone = folder_id.clone();
|
||||
let caller = caller_id.to_string();
|
||||
|
||||
async move {
|
||||
// Acquire semaphore permit
|
||||
let permit = semaphore.acquire().await.unwrap();
|
||||
|
||||
// For both recursive and non-recursive, use the standard delete_folder method
|
||||
// since FolderUseCase only has a single delete_folder method
|
||||
let delete_result = folder_service.delete_folder(&folder_id).await;
|
||||
let delete_result = folder_service.delete_folder(&folder_id, &caller).await;
|
||||
|
||||
// Release the permit explicitly
|
||||
drop(permit);
|
||||
@@ -616,6 +616,7 @@ impl BatchOperationService {
|
||||
&self,
|
||||
folder_ids: Vec<String>,
|
||||
target_folder_id: Option<String>,
|
||||
caller_id: &str,
|
||||
) -> Result<BatchResult<FolderDto>, BatchOperationError> {
|
||||
info!("Starting batch move of {} folders", folder_ids.len());
|
||||
let start_time = std::time::Instant::now();
|
||||
@@ -633,11 +634,12 @@ impl BatchOperationService {
|
||||
let folder_service = self.folder_service.clone();
|
||||
let target = target_folder_id.clone();
|
||||
let semaphore = self.semaphore.clone();
|
||||
let caller = caller_id.to_string();
|
||||
|
||||
async move {
|
||||
let permit = semaphore.acquire().await.unwrap();
|
||||
let dto = MoveFolderDto { parent_id: target };
|
||||
let move_result = folder_service.move_folder(&folder_id, dto).await;
|
||||
let move_result = folder_service.move_folder(&folder_id, dto, &caller).await;
|
||||
drop(permit);
|
||||
(folder_id, move_result)
|
||||
}
|
||||
|
||||
@@ -71,10 +71,30 @@ impl FolderService {
|
||||
)
|
||||
}
|
||||
|
||||
async fn list_folders_for_owner_paginated(
|
||||
&self,
|
||||
_parent_id: Option<&str>,
|
||||
_owner_id: &str,
|
||||
_pagination: &crate::application::dtos::pagination::PaginationRequestDto,
|
||||
) -> Result<
|
||||
crate::application::dtos::pagination::PaginatedResponseDto<FolderDto>,
|
||||
DomainError,
|
||||
> {
|
||||
Ok(
|
||||
crate::application::dtos::pagination::PaginatedResponseDto::new(
|
||||
vec![],
|
||||
0,
|
||||
10,
|
||||
0,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
async fn rename_folder(
|
||||
&self,
|
||||
_id: &str,
|
||||
_dto: RenameFolderDto,
|
||||
_caller_id: &str,
|
||||
) -> Result<FolderDto, DomainError> {
|
||||
Ok(FolderDto::empty())
|
||||
}
|
||||
@@ -83,11 +103,12 @@ impl FolderService {
|
||||
&self,
|
||||
_id: &str,
|
||||
_dto: MoveFolderDto,
|
||||
_caller_id: &str,
|
||||
) -> Result<FolderDto, DomainError> {
|
||||
Ok(FolderDto::empty())
|
||||
}
|
||||
|
||||
async fn delete_folder(&self, _id: &str) -> Result<(), DomainError> {
|
||||
async fn delete_folder(&self, _id: &str, _caller_id: &str) -> Result<(), DomainError> {
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
@@ -211,17 +232,15 @@ impl FolderUseCase for FolderService {
|
||||
pagination: &crate::application::dtos::pagination::PaginationRequestDto,
|
||||
) -> Result<crate::application::dtos::pagination::PaginatedResponseDto<FolderDto>, DomainError>
|
||||
{
|
||||
// Validate and adjust pagination
|
||||
let pagination = pagination.validate_and_adjust();
|
||||
|
||||
// Get paginated folders and total count
|
||||
let (folders, total_items) = self
|
||||
.folder_storage
|
||||
.list_folders_paginated(
|
||||
parent_id,
|
||||
pagination.offset(),
|
||||
pagination.limit(),
|
||||
true, // Always include total for better UX
|
||||
true,
|
||||
)
|
||||
.await
|
||||
.map_err(|e| {
|
||||
@@ -234,10 +253,8 @@ impl FolderUseCase for FolderService {
|
||||
)
|
||||
})?;
|
||||
|
||||
// The total is needed to calculate pagination
|
||||
let total = total_items.unwrap_or(folders.len());
|
||||
|
||||
// Convert to PaginatedResponseDto
|
||||
let response = crate::application::dtos::pagination::PaginatedResponseDto::new(
|
||||
folders.into_iter().map(FolderDto::from).collect(),
|
||||
pagination.page,
|
||||
@@ -248,11 +265,54 @@ impl FolderUseCase for FolderService {
|
||||
Ok(response)
|
||||
}
|
||||
|
||||
/// Renames a folder
|
||||
/// Lists folders with pagination, scoped to a specific owner.
|
||||
async fn list_folders_for_owner_paginated(
|
||||
&self,
|
||||
parent_id: Option<&str>,
|
||||
owner_id: &str,
|
||||
pagination: &crate::application::dtos::pagination::PaginationRequestDto,
|
||||
) -> Result<crate::application::dtos::pagination::PaginatedResponseDto<FolderDto>, DomainError>
|
||||
{
|
||||
let pagination = pagination.validate_and_adjust();
|
||||
|
||||
let (folders, total_items) = self
|
||||
.folder_storage
|
||||
.list_folders_by_owner_paginated(
|
||||
parent_id,
|
||||
owner_id,
|
||||
pagination.offset(),
|
||||
pagination.limit(),
|
||||
true,
|
||||
)
|
||||
.await
|
||||
.map_err(|e| {
|
||||
DomainError::internal_error(
|
||||
"FolderStorage",
|
||||
format!(
|
||||
"Failed to list folders for owner '{}' with pagination in parent {:?}: {}",
|
||||
owner_id, parent_id, e
|
||||
),
|
||||
)
|
||||
})?;
|
||||
|
||||
let total = total_items.unwrap_or(folders.len());
|
||||
|
||||
let response = crate::application::dtos::pagination::PaginatedResponseDto::new(
|
||||
folders.into_iter().map(FolderDto::from).collect(),
|
||||
pagination.page,
|
||||
pagination.page_size,
|
||||
total,
|
||||
);
|
||||
|
||||
Ok(response)
|
||||
}
|
||||
|
||||
/// Renames a folder after verifying ownership.
|
||||
async fn rename_folder(
|
||||
&self,
|
||||
id: &str,
|
||||
dto: RenameFolderDto,
|
||||
caller_id: &str,
|
||||
) -> Result<FolderDto, DomainError> {
|
||||
// Input validation
|
||||
if dto.name.is_empty() {
|
||||
@@ -263,7 +323,7 @@ impl FolderUseCase for FolderService {
|
||||
));
|
||||
}
|
||||
|
||||
// Verify the folder exists
|
||||
// Verify the folder exists and belongs to the caller
|
||||
let existing_folder = self.folder_storage.get_folder(id).await.map_err(|e| {
|
||||
DomainError::internal_error(
|
||||
"FolderStorage",
|
||||
@@ -271,6 +331,14 @@ impl FolderUseCase for FolderService {
|
||||
)
|
||||
})?;
|
||||
|
||||
if existing_folder.owner_id() != Some(caller_id) {
|
||||
tracing::warn!(
|
||||
"rename_folder: user '{}' attempted to rename folder '{}' owned by '{:?}'",
|
||||
caller_id, id, existing_folder.owner_id()
|
||||
);
|
||||
return Err(DomainError::not_found("Folder", id));
|
||||
}
|
||||
|
||||
// Create transaction for renaming
|
||||
let mut transaction = StorageTransaction::new("rename_folder");
|
||||
|
||||
@@ -323,9 +391,9 @@ impl FolderUseCase for FolderService {
|
||||
Ok(FolderDto::from(folder))
|
||||
}
|
||||
|
||||
/// Moves a folder to a new parent
|
||||
async fn move_folder(&self, id: &str, dto: MoveFolderDto) -> Result<FolderDto, DomainError> {
|
||||
// Verify the source folder exists
|
||||
/// Moves a folder to a new parent after verifying ownership.
|
||||
async fn move_folder(&self, id: &str, dto: MoveFolderDto, caller_id: &str) -> Result<FolderDto, DomainError> {
|
||||
// Verify the source folder exists and belongs to the caller
|
||||
let source_folder = self.folder_storage.get_folder(id).await.map_err(|e| {
|
||||
DomainError::internal_error(
|
||||
"FolderStorage",
|
||||
@@ -333,6 +401,14 @@ impl FolderUseCase for FolderService {
|
||||
)
|
||||
})?;
|
||||
|
||||
if source_folder.owner_id() != Some(caller_id) {
|
||||
tracing::warn!(
|
||||
"move_folder: user '{}' attempted to move folder '{}' owned by '{:?}'",
|
||||
caller_id, id, source_folder.owner_id()
|
||||
);
|
||||
return Err(DomainError::not_found("Folder", id));
|
||||
}
|
||||
|
||||
// If a parent_id is specified, verify it exists
|
||||
if let Some(parent_id) = &dto.parent_id {
|
||||
// Verify we are not trying to move the folder into itself or one of its descendants
|
||||
@@ -408,17 +484,23 @@ impl FolderUseCase for FolderService {
|
||||
Ok(FolderDto::from(folder))
|
||||
}
|
||||
|
||||
/// Deletes a folder
|
||||
async fn delete_folder(&self, id: &str) -> Result<(), DomainError> {
|
||||
// Verify the folder exists
|
||||
let _folder = self.folder_storage.get_folder(id).await.map_err(|e| {
|
||||
/// Deletes a folder after verifying ownership.
|
||||
async fn delete_folder(&self, id: &str, caller_id: &str) -> Result<(), DomainError> {
|
||||
// Verify the folder exists and belongs to the caller
|
||||
let folder = self.folder_storage.get_folder(id).await.map_err(|e| {
|
||||
DomainError::internal_error(
|
||||
"FolderStorage",
|
||||
format!("Failed to get folder with ID: {} for deletion: {}", id, e),
|
||||
)
|
||||
})?;
|
||||
|
||||
// In a real implementation, we could verify permissions, dependencies, etc.
|
||||
if folder.owner_id() != Some(caller_id) {
|
||||
tracing::warn!(
|
||||
"delete_folder: user '{}' attempted to delete folder '{}' owned by '{:?}'",
|
||||
caller_id, id, folder.owner_id()
|
||||
);
|
||||
return Err(DomainError::not_found("Folder", id));
|
||||
}
|
||||
|
||||
// Delete the folder
|
||||
self.folder_storage.delete_folder(id).await.map_err(|e| {
|
||||
|
||||
@@ -544,6 +544,18 @@ mod tests {
|
||||
unimplemented!()
|
||||
}
|
||||
|
||||
async fn list_folders_by_owner_paginated(
|
||||
&self,
|
||||
_parent_id: Option<&str>,
|
||||
_owner_id: &str,
|
||||
_offset: usize,
|
||||
_limit: usize,
|
||||
_include_total: bool,
|
||||
) -> Result<(Vec<crate::domain::entities::folder::Folder>, Option<usize>), DomainError>
|
||||
{
|
||||
unimplemented!()
|
||||
}
|
||||
|
||||
async fn rename_folder(
|
||||
&self,
|
||||
_id: &str,
|
||||
|
||||
@@ -373,6 +373,17 @@ impl FolderRepository for MockFolderRepository {
|
||||
Ok((vec![], Some(0)))
|
||||
}
|
||||
|
||||
async fn list_folders_by_owner_paginated(
|
||||
&self,
|
||||
_parent_id: Option<&str>,
|
||||
_owner_id: &str,
|
||||
_offset: usize,
|
||||
_limit: usize,
|
||||
_include_total: bool,
|
||||
) -> std::result::Result<(Vec<Folder>, Option<usize>), DomainError> {
|
||||
Ok((vec![], Some(0)))
|
||||
}
|
||||
|
||||
async fn rename_folder(
|
||||
&self,
|
||||
_id: &str,
|
||||
|
||||
+23
-2
@@ -268,6 +268,17 @@ impl FolderRepository for StubFolderStoragePort {
|
||||
Ok((Vec::new(), Some(0)))
|
||||
}
|
||||
|
||||
async fn list_folders_by_owner_paginated(
|
||||
&self,
|
||||
_parent_id: Option<&str>,
|
||||
_owner_id: &str,
|
||||
_offset: usize,
|
||||
_limit: usize,
|
||||
_include_total: bool,
|
||||
) -> Result<(Vec<Folder>, Option<usize>), DomainError> {
|
||||
Ok((Vec::new(), Some(0)))
|
||||
}
|
||||
|
||||
async fn rename_folder(&self, _id: &str, _new_name: String) -> Result<Folder, DomainError> {
|
||||
Ok(Folder::default())
|
||||
}
|
||||
@@ -374,19 +385,29 @@ impl FolderUseCase for StubFolderUseCase {
|
||||
Ok(PaginatedResponseDto::new(Vec::new(), 0, 10, 0))
|
||||
}
|
||||
|
||||
async fn list_folders_for_owner_paginated(
|
||||
&self,
|
||||
_parent_id: Option<&str>,
|
||||
_owner_id: &str,
|
||||
_pagination: &PaginationRequestDto,
|
||||
) -> Result<PaginatedResponseDto<FolderDto>, DomainError> {
|
||||
Ok(PaginatedResponseDto::new(Vec::new(), 0, 10, 0))
|
||||
}
|
||||
|
||||
async fn rename_folder(
|
||||
&self,
|
||||
_id: &str,
|
||||
_dto: RenameFolderDto,
|
||||
_caller_id: &str,
|
||||
) -> Result<FolderDto, DomainError> {
|
||||
Ok(FolderDto::default())
|
||||
}
|
||||
|
||||
async fn move_folder(&self, _id: &str, _dto: MoveFolderDto) -> Result<FolderDto, DomainError> {
|
||||
async fn move_folder(&self, _id: &str, _dto: MoveFolderDto, _caller_id: &str) -> Result<FolderDto, DomainError> {
|
||||
Ok(FolderDto::default())
|
||||
}
|
||||
|
||||
async fn delete_folder(&self, _id: &str) -> Result<(), DomainError> {
|
||||
async fn delete_folder(&self, _id: &str, _caller_id: &str) -> Result<(), DomainError> {
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -54,6 +54,18 @@ pub trait FolderRepository: Send + Sync + 'static {
|
||||
include_total: bool,
|
||||
) -> Result<(Vec<Folder>, Option<usize>), DomainError>;
|
||||
|
||||
/// Lists folders with pagination, scoped to a specific owner.
|
||||
/// Combines the owner filtering of `list_folders_by_owner` with
|
||||
/// the pagination of `list_folders_paginated`.
|
||||
async fn list_folders_by_owner_paginated(
|
||||
&self,
|
||||
parent_id: Option<&str>,
|
||||
owner_id: &str,
|
||||
offset: usize,
|
||||
limit: usize,
|
||||
include_total: bool,
|
||||
) -> Result<(Vec<Folder>, Option<usize>), DomainError>;
|
||||
|
||||
/// Renames a folder
|
||||
async fn rename_folder(&self, id: &str, new_name: String) -> Result<Folder, DomainError>;
|
||||
|
||||
|
||||
@@ -369,6 +369,82 @@ impl FolderRepository for FolderDbRepository {
|
||||
Ok((folders, total))
|
||||
}
|
||||
|
||||
async fn list_folders_by_owner_paginated(
|
||||
&self,
|
||||
parent_id: Option<&str>,
|
||||
owner_id: &str,
|
||||
offset: usize,
|
||||
limit: usize,
|
||||
include_total: bool,
|
||||
) -> Result<(Vec<Folder>, Option<usize>), DomainError> {
|
||||
let total = if include_total {
|
||||
let count: i64 = if let Some(pid) = parent_id {
|
||||
sqlx::query_scalar(
|
||||
"SELECT COUNT(*) FROM storage.folders WHERE parent_id = $1::uuid AND user_id = $2 AND NOT is_trashed",
|
||||
)
|
||||
.bind(pid)
|
||||
.bind(owner_id)
|
||||
.fetch_one(self.pool())
|
||||
.await
|
||||
} else {
|
||||
sqlx::query_scalar(
|
||||
"SELECT COUNT(*) FROM storage.folders WHERE parent_id IS NULL AND user_id = $1 AND NOT is_trashed",
|
||||
)
|
||||
.bind(owner_id)
|
||||
.fetch_one(self.pool())
|
||||
.await
|
||||
}
|
||||
.map_err(|e| DomainError::internal_error("FolderDb", format!("count_by_owner: {e}")))?;
|
||||
Some(count as usize)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
let rows: Vec<(String, String, Option<String>, String, i64, i64)> = if let Some(pid) = parent_id {
|
||||
sqlx::query_as(
|
||||
r#"
|
||||
SELECT id::text, name, parent_id::text, user_id,
|
||||
EXTRACT(EPOCH FROM created_at)::bigint,
|
||||
EXTRACT(EPOCH FROM updated_at)::bigint
|
||||
FROM storage.folders
|
||||
WHERE parent_id = $1::uuid AND user_id = $2 AND NOT is_trashed
|
||||
ORDER BY name
|
||||
LIMIT $3 OFFSET $4
|
||||
"#,
|
||||
)
|
||||
.bind(pid)
|
||||
.bind(owner_id)
|
||||
.bind(limit as i64)
|
||||
.bind(offset as i64)
|
||||
.fetch_all(self.pool())
|
||||
.await
|
||||
} else {
|
||||
sqlx::query_as(
|
||||
r#"
|
||||
SELECT id::text, name, parent_id::text, user_id,
|
||||
EXTRACT(EPOCH FROM created_at)::bigint,
|
||||
EXTRACT(EPOCH FROM updated_at)::bigint
|
||||
FROM storage.folders
|
||||
WHERE parent_id IS NULL AND user_id = $1 AND NOT is_trashed
|
||||
ORDER BY name
|
||||
LIMIT $2 OFFSET $3
|
||||
"#,
|
||||
)
|
||||
.bind(owner_id)
|
||||
.bind(limit as i64)
|
||||
.bind(offset as i64)
|
||||
.fetch_all(self.pool())
|
||||
.await
|
||||
}
|
||||
.map_err(|e| DomainError::internal_error("FolderDb", format!("paginate_by_owner: {e}")))?;
|
||||
|
||||
let mut folders = Vec::with_capacity(rows.len());
|
||||
for (id, name, pid, uid, ca, ma) in rows {
|
||||
folders.push(self.row_to_folder(id, name, pid, Some(uid), ca, ma).await?);
|
||||
}
|
||||
Ok((folders, total))
|
||||
}
|
||||
|
||||
async fn rename_folder(&self, id: &str, new_name: String) -> Result<Folder, DomainError> {
|
||||
sqlx::query(
|
||||
r#"
|
||||
|
||||
@@ -258,6 +258,7 @@ pub async fn delete_files_batch(
|
||||
/// Handler for deleting multiple folders in batch
|
||||
pub async fn delete_folders_batch(
|
||||
State(state): State<BatchHandlerState>,
|
||||
auth_user: AuthUser,
|
||||
Json(request): Json<BatchFolderOperationRequest>,
|
||||
) -> ApiResult<impl IntoResponse> {
|
||||
// Verify there are folders to process
|
||||
@@ -274,7 +275,7 @@ pub async fn delete_folders_batch(
|
||||
// Execute batch operation
|
||||
let result = state
|
||||
.batch_service
|
||||
.delete_folders(request.folder_ids, request.recursive)
|
||||
.delete_folders(request.folder_ids, request.recursive, &auth_user.id)
|
||||
.await
|
||||
.map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()))?;
|
||||
|
||||
@@ -555,6 +556,7 @@ pub async fn trash_batch(
|
||||
/// Handler for moving multiple folders in batch
|
||||
pub async fn move_folders_batch(
|
||||
State(state): State<BatchHandlerState>,
|
||||
auth_user: AuthUser,
|
||||
Json(request): Json<BatchFolderOperationRequest>,
|
||||
) -> ApiResult<impl IntoResponse> {
|
||||
if request.folder_ids.is_empty() {
|
||||
@@ -569,7 +571,7 @@ pub async fn move_folders_batch(
|
||||
|
||||
let result = state
|
||||
.batch_service
|
||||
.move_folders(request.folder_ids, request.target_folder_id)
|
||||
.move_folders(request.folder_ids, request.target_folder_id, &auth_user.id)
|
||||
.await
|
||||
.map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()))?;
|
||||
|
||||
|
||||
@@ -13,7 +13,7 @@ use crate::application::ports::inbound::FolderUseCase;
|
||||
use crate::application::services::folder_service::FolderService;
|
||||
use crate::common::di::AppState as GlobalAppState;
|
||||
use crate::common::errors::ErrorKind;
|
||||
use crate::interfaces::middleware::auth::{AuthUser, OptionalAuthUser};
|
||||
use crate::interfaces::middleware::auth::AuthUser;
|
||||
|
||||
type AppState = Arc<FolderService>;
|
||||
|
||||
@@ -136,15 +136,14 @@ impl FolderHandler {
|
||||
}
|
||||
|
||||
/// Lists contents of a specific folder with pagination.
|
||||
/// Scoped to the authenticated user — only returns folders owned by this user.
|
||||
pub async fn list_folder_contents_paginated(
|
||||
State(service): State<AppState>,
|
||||
_auth_user: AuthUser,
|
||||
auth_user: AuthUser,
|
||||
Path(id): Path<String>,
|
||||
pagination: Query<PaginationRequestDto>,
|
||||
) -> axum::response::Response {
|
||||
// For sub-folder pagination, use the standard paginated path
|
||||
// (owner filtering is implicit — sub-folders inherit ownership)
|
||||
match service.list_folders_paginated(Some(&id), &pagination).await {
|
||||
match service.list_folders_for_owner_paginated(Some(&id), &auth_user.id, &pagination).await {
|
||||
Ok(paginated_result) => (StatusCode::OK, Json(paginated_result)).into_response(),
|
||||
Err(err) => {
|
||||
let status = match err.kind {
|
||||
@@ -184,13 +183,14 @@ impl FolderHandler {
|
||||
}
|
||||
}
|
||||
|
||||
/// Renames a folder
|
||||
/// Renames a folder (ownership enforced by service layer)
|
||||
pub async fn rename_folder(
|
||||
State(service): State<AppState>,
|
||||
auth_user: AuthUser,
|
||||
Path(id): Path<String>,
|
||||
Json(dto): Json<RenameFolderDto>,
|
||||
) -> impl IntoResponse {
|
||||
match service.rename_folder(&id, dto).await {
|
||||
match service.rename_folder(&id, dto, &auth_user.id).await {
|
||||
Ok(folder) => (StatusCode::OK, Json(folder)).into_response(),
|
||||
Err(err) => {
|
||||
let status = match err.kind {
|
||||
@@ -211,13 +211,14 @@ impl FolderHandler {
|
||||
}
|
||||
}
|
||||
|
||||
/// Moves a folder to a new parent
|
||||
/// Moves a folder to a new parent (ownership enforced by service layer)
|
||||
pub async fn move_folder(
|
||||
State(service): State<AppState>,
|
||||
auth_user: AuthUser,
|
||||
Path(id): Path<String>,
|
||||
Json(dto): Json<MoveFolderDto>,
|
||||
) -> impl IntoResponse {
|
||||
match service.move_folder(&id, dto).await {
|
||||
match service.move_folder(&id, dto, &auth_user.id).await {
|
||||
Ok(folder) => (StatusCode::OK, Json(folder)).into_response(),
|
||||
Err(err) => {
|
||||
let status = match err.kind {
|
||||
@@ -231,13 +232,13 @@ impl FolderHandler {
|
||||
}
|
||||
}
|
||||
|
||||
/// Deletes a folder (with trash support)
|
||||
/// Deletes a folder (ownership enforced by service layer)
|
||||
pub async fn delete_folder(
|
||||
State(service): State<AppState>,
|
||||
auth_user: AuthUser,
|
||||
Path(id): Path<String>,
|
||||
) -> impl IntoResponse {
|
||||
// For folder deletion without trash functionality
|
||||
match service.delete_folder(&id).await {
|
||||
match service.delete_folder(&id, &auth_user.id).await {
|
||||
Ok(_) => StatusCode::NO_CONTENT.into_response(),
|
||||
Err(err) => {
|
||||
let status = match err.kind {
|
||||
@@ -250,16 +251,13 @@ impl FolderHandler {
|
||||
}
|
||||
}
|
||||
|
||||
/// Deletes a folder with trash functionality
|
||||
/// Deletes a folder with trash functionality (ownership enforced by service layer)
|
||||
pub async fn delete_folder_with_trash(
|
||||
State(state): State<GlobalAppState>,
|
||||
OptionalAuthUser(auth_user): OptionalAuthUser,
|
||||
auth_user: AuthUser,
|
||||
Path(id): Path<String>,
|
||||
) -> impl IntoResponse {
|
||||
let user_id = auth_user
|
||||
.as_ref()
|
||||
.map(|u| u.id.as_str())
|
||||
.unwrap_or("anonymous");
|
||||
let user_id = &auth_user.id;
|
||||
// Check if trash service is available
|
||||
if let Some(trash_service) = &state.trash_service {
|
||||
tracing::info!("Moving folder to trash: {}", id);
|
||||
@@ -282,7 +280,7 @@ impl FolderHandler {
|
||||
|
||||
// Fallback to permanent delete if trash is unavailable or failed
|
||||
let folder_service = &state.applications.folder_service;
|
||||
match folder_service.delete_folder(&id).await {
|
||||
match folder_service.delete_folder(&id, user_id).await {
|
||||
Ok(_) => {
|
||||
tracing::info!("Folder permanently deleted: {}", id);
|
||||
StatusCode::NO_CONTENT.into_response()
|
||||
@@ -306,19 +304,32 @@ impl FolderHandler {
|
||||
}
|
||||
}
|
||||
|
||||
/// Downloads a folder as a ZIP file
|
||||
/// Downloads a folder as a ZIP file (ownership enforced)
|
||||
pub async fn download_folder_zip(
|
||||
State(state): State<GlobalAppState>,
|
||||
auth_user: AuthUser,
|
||||
Path(id): Path<String>,
|
||||
Query(_params): Query<HashMap<String, String>>,
|
||||
) -> impl IntoResponse {
|
||||
tracing::info!("Downloading folder as ZIP: {}", id);
|
||||
|
||||
// Get folder information first to check it exists and get name
|
||||
// Get folder information and verify ownership
|
||||
let folder_service = &state.applications.folder_service;
|
||||
|
||||
match folder_service.get_folder(&id).await {
|
||||
Ok(folder) => {
|
||||
// Access check: folder must belong to the requesting user
|
||||
if folder.owner_id.as_deref() != Some(&auth_user.id) {
|
||||
tracing::warn!(
|
||||
"download_folder_zip: user '{}' attempted to download folder '{}' owned by '{:?}'",
|
||||
auth_user.id, id, folder.owner_id
|
||||
);
|
||||
return (
|
||||
StatusCode::NOT_FOUND,
|
||||
Json(serde_json::json!({ "error": "Folder not found" })),
|
||||
)
|
||||
.into_response();
|
||||
}
|
||||
tracing::info!("Preparing ZIP for folder: {} ({})", folder.name, id);
|
||||
|
||||
// Use ZIP service from DI container
|
||||
|
||||
@@ -645,9 +645,10 @@ async fn handle_delete(
|
||||
let folder_result = folder_service.get_folder_by_path(&path).await;
|
||||
|
||||
if let Ok(folder) = folder_result {
|
||||
// Delete folder
|
||||
// Delete folder — use the folder's own owner as caller_id
|
||||
let caller_id = folder.owner_id.as_deref().unwrap_or("webdav");
|
||||
folder_service
|
||||
.delete_folder(&folder.id)
|
||||
.delete_folder(&folder.id, caller_id)
|
||||
.await
|
||||
.map_err(|e| AppError::internal_error(format!("Failed to delete folder: {}", e)))?;
|
||||
} else {
|
||||
@@ -761,7 +762,7 @@ async fn handle_move(
|
||||
};
|
||||
|
||||
folder_service
|
||||
.move_folder(&folder.id, move_dto)
|
||||
.move_folder(&folder.id, move_dto, folder.owner_id.as_deref().unwrap_or("webdav"))
|
||||
.await
|
||||
.map_err(|e| AppError::internal_error(format!("Failed to move folder: {}", e)))?;
|
||||
|
||||
@@ -771,7 +772,7 @@ async fn handle_move(
|
||||
};
|
||||
|
||||
folder_service
|
||||
.rename_folder(&folder.id, rename_dto)
|
||||
.rename_folder(&folder.id, rename_dto, folder.owner_id.as_deref().unwrap_or("webdav"))
|
||||
.await
|
||||
.map_err(|e| AppError::internal_error(format!("Failed to rename folder: {}", e)))?;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user