feat: folder ownership scoping, batch operations integration, frontend audit fixes

Backend:
- Add owner_id to Folder entity + FolderDto (DB user_id column)
- Add list_folders_by_owner to FolderRepository trait + PG impl
- Add list_folders_for_owner to FolderUseCase + FolderService
- Rewrite FolderHandler: all endpoints now scope by AuthUser
- Remove dead handler methods (list_folders_inner, list_folders_for_user, is_user_home_folder, folder_belongs_to_user)
- Add ownership check in get_folder (returns 404 on mismatch)

Batch operations:
- Add trash_service + zip_service to BatchOperationService
- New methods: trash_files, trash_folders, move_folders, download_zip
- New handlers: trash_batch, move_folders_batch, download_batch
- New routes: POST /api/batch/trash, /api/batch/folders/move, /api/batch/download

Frontend:
- Replace findUserHomeFolder (~130 lines) with resolveHomeFolder (~35 lines)
- Remove client-side folder filtering in loadFiles (backend now scopes)
- Rewrite batchDelete: N requests -> 1 POST /api/batch/trash
- Rewrite batchMove: N requests -> 2 POST max (files + folders)
- Rewrite batchDownload: N requests -> 1 POST /api/batch/download (ZIP)
- Search moved to backend, share system uses backend API
- Dark mode fixes, frontend audit improvements
This commit is contained in:
Dionisio
2026-02-15 23:45:11 +01:00
parent 6e1b77f244
commit 7737ed90c7
33 changed files with 3078 additions and 1958 deletions
+54 -42
View File
@@ -308,46 +308,36 @@ const multiSelect = {
});
if (!confirmed) return;
let success = 0;
let errors = 0;
const fileIds = items.filter(i => i.type === 'file').map(i => i.id);
const folderIds = items.filter(i => i.type === 'folder').map(i => i.id);
for (const item of items) {
try {
const endpoint = item.type === 'folder'
? `/api/trash/folders/${item.id}`
: `/api/trash/files/${item.id}`;
try {
const response = await fetch('/api/batch/trash', {
method: 'POST',
headers: { ...getAuthHeaders(), 'Content-Type': 'application/json' },
body: JSON.stringify({ file_ids: fileIds, folder_ids: folderIds })
});
const response = await fetch(endpoint, {
method: 'DELETE',
headers: getAuthHeaders()
});
const data = await response.json();
const success = data.stats?.successful || 0;
const errors = data.stats?.failed || 0;
if (response.ok) {
success++;
} else {
// Fallback to direct delete
const fallback = item.type === 'folder'
? `/api/folders/${item.id}`
: `/api/files/${item.id}`;
const r2 = await fetch(fallback, { method: 'DELETE', headers: getAuthHeaders() });
if (r2.ok) success++;
else errors++;
}
} catch (e) {
console.error('Error deleting item:', item, e);
errors++;
this.clear();
window.loadFiles();
if (errors > 0) {
const failedNames = (data.failed || []).map(f => f.id).join(', ');
window.ui.showNotification('Batch delete',
`${success} moved to trash, ${errors} failed`);
} else {
window.ui.showNotification('Moved to trash',
`${success} item${success !== 1 ? 's' : ''} moved to trash`);
}
}
this.clear();
window.loadFiles();
if (errors > 0) {
window.ui.showNotification('Batch delete',
`${success} moved to trash, ${errors} failed`);
} else {
window.ui.showNotification('Moved to trash',
`${success} item${success !== 1 ? 's' : ''} moved to trash`);
} catch (e) {
console.error('Batch trash error:', e);
window.ui.showNotification('Error', 'Could not move items to trash');
this.clear();
window.loadFiles();
}
},
@@ -379,17 +369,39 @@ const multiSelect = {
dialog.style.display = 'flex';
},
/** Batch download — downloads each item individually */
/** Batch download — downloads all selected items as a single ZIP */
async batchDownload() {
const items = this.items;
if (items.length === 0) return;
for (const item of items) {
if (item.type === 'folder') {
await window.fileOps.downloadFolder(item.id, item.name);
} else {
await window.fileOps.downloadFile(item.id, item.name);
window.ui.showNotification('Preparing download', 'Creating ZIP archive...');
try {
const fileIds = items.filter(i => i.type === 'file').map(i => i.id);
const folderIds = items.filter(i => i.type === 'folder').map(i => i.id);
const response = await fetch('/api/batch/download', {
method: 'POST',
headers: { ...getAuthHeaders(), 'Content-Type': 'application/json' },
body: JSON.stringify({ file_ids: fileIds, folder_ids: folderIds })
});
if (!response.ok) {
throw new Error(`Server returned ${response.status}`);
}
const blob = await response.blob();
const url = URL.createObjectURL(blob);
const link = document.createElement('a');
link.href = url;
link.download = `oxicloud-download-${Date.now()}.zip`;
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
URL.revokeObjectURL(url);
} catch (e) {
console.error('Batch download error:', e);
window.ui.showNotification('Error', 'Could not download selected items');
}
},