Commit Graph

100 Commits

Author SHA1 Message Date
Dionisio 48d853360e feat: implement OAuth 2.0 Device Authorization Grant (RFC 8628) for WebDAV/CalDAV/CardDAV
Adds full Device Authorization Grant flow so DAV clients (rclone, etc.)
can authenticate without browser-based OAuth redirects.

New files:
- Domain entity: DeviceCode with status lifecycle (pending/authorized/denied/expired)
- Port: DeviceCodeStoragePort trait (7 async methods)
- DTOs: request/response types for all device auth endpoints
- Repository: DeviceCodePgRepository (PostgreSQL implementation)
- Service: DeviceAuthService (initiate, verify, approve, deny, poll, cleanup)
- Handler: 6 HTTP endpoints (2 public + 4 protected)
- Static: device-verify.html verification page served at /device

Flow:
1. Client POST /api/auth/device/authorize → device_code + user_code
2. User opens /device?code=XXXX in browser, approves
3. Client polls POST /api/auth/device/token → receives JWT tokens
4. Client uses Bearer token with existing WebDAV/CalDAV/CardDAV middleware

Schema: auth.device_codes table + device_code_status enum added to schema.sql

Closes #152
2026-03-01 11:54:43 +01:00
Dionisio 2421724b80 style: cargo fmt --all 2026-02-26 01:02:25 +01:00
Dionisio b05805ee1c fix: resolve clippy warnings (redundant closure, collapsible if) 2026-02-26 00:57:49 +01:00
Dionisio 6df68d3716 style: cargo fmt --all 2026-02-26 00:47:32 +01:00
Dionisio 7e278a0b32 perf: bulk-delete expired trash in 2 SQL queries instead of N+1 loop 2026-02-26 00:42:41 +01:00
Dionisio 9f8a6f5177 perf: stream_files_in_subtree — replace Vec<File> with async Stream
Replace list_files_in_subtree (fetch_all → Vec) with stream_files_in_subtree
that returns a Pin<Box<dyn Stream<Item = Result<File/FileDto>>>> backed by a
PostgreSQL cursor via sqlx::fetch().

Changes:
- FileReadPort::stream_files_in_subtree() returns streaming cursor (no default)
- FileRetrievalUseCase::stream_files_in_subtree() maps File→FileDto on the fly
- FileBlobReadRepository: async_stream::try_stream! + sqlx::fetch() cursor
- batch_operations: consume stream into HashMap incrementally
- zip_service: consume stream into HashMap incrementally
- All stubs/mocks updated (return empty stream)

Eliminates:
- Double allocation: Vec<(9-tuple)> + Vec<File> materialized simultaneously
- Unbounded RAM proportional to subtree size (was ~500 bytes × N files)
- Latency: callers blocked until last row fetched from PG

RAM is now O(folders) for the HashMap, not O(files).
2026-02-26 00:07:10 +01:00
Dionisio 5b55056921 perf: Arc-wrap SearchResultsDto for zero-copy cache reads
- Change SearchUseCase::search() return type to Arc<SearchResultsDto>
- Replace Cache<u64, SearchResultsDto> with Cache<u64, Arc<SearchResultsDto>>
- Eliminate .clone() on full result set; use Arc::clone() (ptr bump)
- Remove Clone derive from SearchResultsDto (no longer needed)
- Update handlers to deref Arc for JSON serialization
2026-02-25 23:47:51 +01:00
Dionisio 5a1959bf23 perf: eliminate Vec<u8> buffer paths — all uploads now stream to disk
Issue #4 (HIGH): save_file(Vec<u8>) and update_file_content(Vec<u8>)
accepted up to 10 MB of contiguous memory per request. While the main
upload paths already used streaming, the WebDAV compat methods
(create_file, update_file) and the empty-file handler still used the
buffered path, creating a .to_vec() copy.

Changes:
- FileWritePort trait: remove save_file(Vec<u8>) and
  update_file_content(Vec<u8>) — only streaming variants remain
- FileUploadUseCase trait: remove upload_file(Vec<u8>)
- file_upload_service.rs: create_file() and update_file() now spool
  &[u8] to NamedTempFile + Sha256::digest, then delegate to streaming
  path (save_file_from_temp / update_file_streaming)
- file_handler.rs: empty file uploads use upload_file_streaming with
- FileBlobWriteRepository: remove save_file and update_file_content impls
- StubFileWritePort, StubFileUploadUseCase, MockFileRepository: remove
  corresponding dead method impls

Impact: impossible to accidentally use a buffered upload path. All
content goes through streaming with ~256 KB peak RAM. -166 LOC.
2026-02-25 23:41:16 +01:00
Dionisio 5f883aa0f8 perf: rewrite batch download_zip to stream via temp file instead of RAM
- Replace in-memory Cursor<Vec<u8>> with async_zip + NamedTempFile (O(256KB) RAM)
- Stream ZIP to client via ReaderStream instead of materializing entire archive
- Replace N+1 BFS folder traversal with 2 bulk ltree queries
- Remove dead zip_service field and with_zip_service() from BatchOperationService
- Remove synchronous zip crate dependency (only async_zip remains)
- Eliminates ~2.5GB RAM spike per 2GB batch download
2026-02-25 23:12:19 +01:00
George Wu d2e6dbdee6 style: run cargo fmt 2026-02-25 08:58:51 -08:00
Dionisio 6479da35f2 fix: update trash test mocks to simulate PG CASCADE on clear_trash
MockTrashRepository now holds shared Arc<Mutex> refs to the
trashed_files and trashed_folders collections so that clear_trash()
also purges them — mirroring the ON DELETE CASCADE + PG trigger
behaviour in production.

All 112 tests pass.
2026-02-25 10:49:20 +01:00
Dionisio 97cf6402e2 fix: resolve all clippy warnings and enforce cargo fmt
- display_helpers: convert module doc-comments to regular comments,
  merge identical text/markdown + text/ branches
- search_service: replace needless range loops with slice-based pagination
- folder_repository, folder_db_repository: collapse nested if statements
- favorites_pg_repository: remove unnecessary borrow on generic arg
- file_blob_read_repository: collapse 6 nested if-let blocks
- file_blob_write_repository: collapse nested if for dedup ref decrement
- chunked_upload_service: use div_ceil(), collapse 2 nested if blocks
- folder_handler: collapse nested if-let for owner check
- webdav_handler: replace 7x io::Error::new(ErrorKind::Other, ..) with
  io::Error::other(..)
- cargo fmt applied to all files

Passes: cargo clippy --all-targets --all-features -- -D warnings
2026-02-25 10:28:34 +01:00
Dionisio 093400ce72 perf: optimize empty_trash — remove redundant N+1 loop, rely on bulk clear_trash()
The existing clear_trash() already performs bulk SQL DELETEs:
  1. DELETE FROM storage.files  WHERE user_id = $1 AND is_trashed = TRUE
  2. DELETE FROM storage.folders WHERE user_id = $1 AND is_trashed = TRUE

The per-item loop (get_trash_items + N individual deletes) was redundant
since clear_trash() re-deleted everything anyway. Removed the loop entirely.

Folder CASCADE (FK ON DELETE CASCADE) handles child folders and their files.
PG trigger trg_files_decrement_blob_ref automatically decrements blob
ref_counts for every deleted file row — no Rust-side remove_reference()
call needed. Orphan blobs are cleaned by garbage collection.

Result: O(2) SQL queries instead of O(N+2) for any trash size.
2026-02-25 10:02:35 +01:00
Dionisio 9f692f03c3 Implement dual DB pools (primary + maintenance) and wire services 2026-02-24 19:28:00 +01:00
Dionisio 28966ce28e optimize folder search: SQL-level filtering, user isolation, no in-memory filter; batch cascade trigger 2026-02-24 17:15:36 +01:00
Dionisio 71c2cb5edb perf: Arc<AppState>, streaming PROPFIND, spawn_blocking SHA-256
- Issue #4: Wrap AppState in Arc — eliminates 42 Arc::clone + 16 String::clone per request
- Issue #2: Reject Depth:infinity with 403 + streaming XML with paginated DB queries
- Issue #5: Move chunked upload assembly (SHA-256 hash-on-write) to spawn_blocking
- Remove ~270 lines dead code from di.rs (unused builders, Default impl, stubs)
- Clean up unused tokio imports in chunked_upload_service.rs
2026-02-24 15:11:56 +01:00
Dionisio cace61127f perf(cache): use Arc<str> for etag/content_type in ContentCachePort
Replace String with Arc<str> for etag and content_type fields in the
content cache. String::clone() allocates and copies the full string on
every cache hit (O(n)), while Arc<str>::clone() is O(1) — just an atomic
ref-count increment.

This eliminates 2 heap allocations per cache hit on the hottest download
path. At 1000 req/s that is 2000 fewer alloc/dealloc cycles per second.

Changed files:
- cache_ports.rs: trait signatures String → Arc<str>
- file_content_cache.rs: CacheEntry fields, get/put methods, tests
- stubs.rs: StubContentCachePort signatures
- file_retrieval_service.rs: caller creates Arc<str> before put()
2026-02-24 13:22:04 +01:00
Dionisio cd5b937065 perf(search): migrate moka::sync::Cache to moka::future::Cache
sync::Cache uses an internal Mutex that blocks the Tokio worker thread
during maintenance (eviction, frequency bookkeeping). Under high
concurrency this serialises all tasks scheduled on the same worker.

future::Cache defers maintenance to an internal async task and never
holds a synchronous lock visible to the caller.

Changes:
- Field type: sync::Cache → future::Cache
- get_from_cache / store_in_cache: now async, calls .await
- clear_search_cache: added run_pending_tasks().await after invalidate

Consistent with the pattern already used in thumbnail_service,
image_transcode_service, and file_content_cache.
2026-02-24 12:58:47 +01:00
Dionisio 0986ebf81c perf(transcode): eliminate double buffer copy by accepting Bytes instead of &[u8]
Change ImageTranscodePort::get_transcoded signature from &[u8] to Bytes.
- Rayon closure now receives Bytes::clone() (O(1) ref-count) instead of .to_vec() (~5 MB copy)
- Fallback path returns owned Bytes directly instead of Bytes::from(to_vec()) (~5 MB copy)
- Caller passes content.clone() (O(1)) instead of implicit deref

Saves ~10 MB of allocation per transcode call on a 5 MB image.
2026-02-24 12:48:08 +01:00
Dionisio ed433df2af perf(zip): eliminate N+1 queries with ltree bulk subtree fetch
Replace BFS traversal that issued 2 SQL queries per folder (list_files +
list_folders) with 2 total queries using PostgreSQL ltree <@ operator:

1. list_subtree_folders: single GiST-indexed scan for all folders
2. list_files_in_subtree: single GiST-indexed join for all files

Files are grouped by folder_id in a HashMap, then iterated in directory
order (folders pre-sorted by path from SQL).

Changes across 4 architecture layers:
- Domain: FolderRepository::list_subtree_folders (default impl)
- Application ports: FolderUseCase, FileRetrievalUseCase, FileReadPort
- Application services: FolderService, FileRetrievalService passthroughs
- Infrastructure: PG implementations + ZipService rewrite

Query count: O(N) → O(1). Latency for 100-folder tree: ~200 round-trips → 3.
2026-02-24 12:18:38 +01:00
Dionisio a79700b11c perf(auth): add Semaphore to bound concurrent Argon2 hashes
- Wrap hash_password calls with tokio::sync::Semaphore (max 2 concurrent)
- Prevents Argon2 from monopolizing CPU cores and starving async tasks
- Caps peak RAM from unbounded to ~38 MB for concurrent hash operations
- Uses spawn_blocking via injected PasswordHasherPort to avoid blocking Tokio
2026-02-24 12:18:20 +01:00
Dionisio 44113eabdc perf(#12): push suggest() filtering + LIMIT to SQL
Replace the in-memory approach (list_files → filter in Rust) with
SQL-level ILIKE + relevance ORDER BY + LIMIT.

- Add suggest_files_by_name() to FileReadPort (default impl for stubs)
- Add suggest_folders_by_name() to FolderRepository (default impl)
- Implement efficient SQL in FileBlobReadRepository & FolderDbRepository
  with LOWER(name) LIKE pattern, 3-tier relevance sorting, and LIMIT
- Rewrite SearchService::suggest() to call new methods in parallel

Before: 50K files → ~10 MB transferred, 52K string comparisons
After:  50K files → ~20 rows transferred, index-assisted scan
2026-02-23 23:36:38 +01:00
Diocrafts 85908311dc perf: findings 6.1, 6.2, 2.6 — async Argon2, moka cache, full streaming migration
- 6.1: PasswordHasherPort now async_trait with spawn_blocking for Argon2
- 6.2: OIDC pending maps migrated from std::sync::Mutex to moka::sync::Cache with TTL
- 2.6: All file download paths migrated to 64KB streaming (get_file_stream / read_blob_stream)
  - WOPI, dedup, batch ZIP, file_retrieval_service consumers migrated
  - WebDAV COPY uses zero-copy dedup (copy_file)
  - Removed dead code: get_file_content, get_file_mmap, read_blob, read_blob_bytes
    from traits, impls, stubs, and mocks (18 files touched)
2026-02-23 00:51:46 +01:00
Diocrafts b501c4052b perf: replace recursive spawn-per-folder search with O(1) ltree queries
- Add search_files_in_subtree() to FileReadPort with ltree-based SQL
- Add list_descendant_folders() to FolderRepository with ltree GiST index
- Implement both in PG repositories (single query per entity type)
- Remove search_parallel() fan-out (O(N) tokio::spawn → 0 spawns)
- Remove passes_file_filter/passes_folder_filter (filtering now in SQL)
- Fix SearchCriteriaDto cache key: JSON serialization → u64 hash (15x faster)
- Fix escaped quote literals in webdav_handler.rs handle_put function
2026-02-23 00:17:40 +01:00
Diocrafts 92e0364a60 fix: OOM protection, lock-free thumbnail cache, OIDC JWKS TTL
- Streaming WebDAV PUT: body spooled to tempfile with incremental SHA-256,
  peak RAM ~64KB regardless of file size (Solution 2)
- RequestBodyLimitLayer (1MB) on CalDAV/CardDAV routers (Solution 3)
- All body::to_bytes(body, usize::MAX) replaced with explicit limits:
  PROPFIND/PROPPATCH/LOCK → 1MB, MKCOL → 4KB
- Added AppError::payload_too_large (HTTP 413)
- Added max_upload_size to StorageConfig (default 10GB, env override)
- New streaming update chain: FileWritePort::update_file_content_from_temp
  → FileUploadUseCase::update_file_streaming
- ThumbnailService: migrated from RwLock<LruCache> to moka::future::Cache
  with weight-based eviction — eliminates lock contention on read hot-path
- OIDC: discovery + JWKS caches now expire after 1 hour (Cached<T> wrapper)
  so IdP key rotation no longer requires server restart
2026-02-22 23:28:03 +01:00
Diocrafts 6ad23e0acc fix(perf): replace std::sync::Mutex with moka lock-free cache in async context
Eliminates deadlock risk under concurrent load:
- SearchService: Arc<Mutex<HashMap>> → moka::sync::Cache with automatic TTL + LRU
  - Removed manual cleanup task, TTL checking, eviction logic (~90 lines)
  - get_from_cache/store_in_cache are now single lock-free calls
  - clear_search_cache uses invalidate_all()
- HttpCache: Arc<Mutex<HashMap>> → moka::sync::Cache
  - Removed stats(), cleanup(), evict_oldest() manual methods
  - Removed CacheEntry.timestamp/max_age fields (moka handles internally)
  - Removed start_cache_cleanup_task (moka evicts lazily)
- routes.rs: Removed dead HttpCache instantiation and unused TTL variables

Impact: std::sync::Mutex::lock() blocked Tokio worker threads; N concurrent
requests (N = CPU count) could freeze the entire server. moka::sync::Cache
is lock-free and designed for async runtimes — zero contention.
2026-02-22 22:37:36 +01:00
Diocrafts 2dd3dc0b54 fix: prevent blob storage leak on folder deletion
- Add PG trigger trg_files_decrement_blob_ref (AFTER DELETE ON storage.files)
  that auto-decrements storage.blobs.ref_count for every deleted file row.
  Covers all paths: explicit DELETE, ON DELETE CASCADE, trash emptying.

- Remove manual remove_reference() call from delete_file() in
  file_blob_write_repository — trigger is now the single source of truth.

- Fix double-decrement bug in FileManagementService::delete_with_cleanup:
  was decrementing ref_count on trash (soft-delete) when the file row still
  existed, causing premature blob GC and potential data corruption on restore.

- Remove dead fields (file_read, dedup_service) from FileManagementService
  and simplify constructors — ref_count fully handled by PG trigger.
2026-02-22 22:07:46 +01:00
Dionisio Pozo 0975f1f7ab Merge pull request #141 from gbw/docs/env-file-documentation
docs: add example.env and document all environment variables
2026-02-22 09:48:36 +01:00
Dionisio Pozo 316b48b8e4 Merge pull request #139 from gbw/database_level_filtering
Optimize search service with database-level pagination and improve cache handling
2026-02-22 09:47:09 +01:00
Dionisio Pozo 959337479e Merge pull request #140 from gbw/fix/storage-usage-calculation
fix: correct storage usage calculation by using direct SQL query
2026-02-22 09:45:39 +01:00
George Wu 2c428678b6 docs: add example.env and document all environment variables
- Create example.env with all 31 environment variables documented
- Update docker-compose.yml to use env_file directive
- Update doc/deployment.md with Quick Start and missing variables
- Add OXICLOUD_BASE_URL and WOPI configuration docs
- Update README.md Docker section with .env setup
- Remove duplicate port mapping (incorporates 4577e56)
- Fix server port default from 8085 to 8086
2026-02-21 18:44:38 -08:00
George Wu be290604bf feat(search): add missing trait methods for mock repositories
- Add search_files_paginated and count_files to MockFileRepository in share_service.rs
- Add search_files_paginated and count_files to MockFileRepository in trash_service_test.rs

These methods were added to the FileReadPort trait to support database-level
pagination for search optimization.
2026-02-21 18:01:08 -08:00
George Wu 922f00e339 fix: remove uuid cast from storage usage query
The user_id column in storage.files is varchar, not uuid. The ::uuid cast
was causing a type mismatch error when calculating storage usage.
2026-02-21 17:49:36 -08:00
George Wu 6907f0a057 fix: correct storage usage calculation by using direct SQL query
The original implementation tried to find user's home folder via
file_repository.list_files(None), but this only returns files (not folders)
and folders are in a separate table. This resulted in storage always
showing 0 bytes.

Fixed by using a direct SQL query to sum file sizes by user_id from
storage.files table, which is more efficient (O(1) vs recursive) and
correctly calculates storage for all files owned by the user.
2026-02-21 17:30:32 -08:00
George Wu f3f5c40b6b fix: auto-create home folder when listing root folders returns empty
When a user has no home folder (e.g., legacy users or failed folder
creation during registration), the frontend would get an empty list
from GET /api/folders, leaving userHomeFolderId undefined. This caused
uploads to fail or go to the wrong location.

Now list_folders_for_owner() automatically creates a home folder when:
- Listing root folders (parent_id is None)
- The result is empty

This self-healing approach fixes the issue at the source, ensuring
the frontend always gets a valid userHomeFolderId.
2026-02-21 16:53:08 -08:00
George Wu 95fa648a55 Optimize search service with database-level pagination and improve cache handling
- Add search_files_paginated method to FileReadPort for database-level pagination
- Implement efficient SQL-based search with LIMIT/OFFSET in file_blob_read_repository
- Add relevance scoring (exact match > starts-with > contains)
- Support multiple sort options (name, date, size) with ascending/descending order
- Fix cache expiration handling with proper borrow checker semantics
- Use i64 for SQL LIMIT/OFFSET parameters instead of usize
- Clean up duplicate SQL query builder code in search_files_paginated

This significantly improves search performance for non-recursive queries by:
- Pushing pagination to the database layer
- Avoiding loading all files into memory for filtering
- Supporting database-level sorting
2026-02-21 14:50:53 -08:00
George Wu 46a65c322c Add database level pagination and filtering for efficiency 2026-02-21 14:50:53 -08:00
Dionisio Pozo ef15fe85cb Merge pull request #135 from zjean/feature/oidc-user-identity
Feature/OIDC user identity
2026-02-21 21:08:57 +01:00
George Wu 52a47d4b33 Add email_verified check for OIDC login
- Parse email_verified from ID token and UserInfo endpoint
- Reject OIDC login if email is present and not verified
- Only applies when email is in OIDC claims (not required otherwise)
2026-02-21 11:51:48 -08:00
Jan Wiebe 0db9c3cd88 feat(auth): expose auth_provider in UserDto and guard password ops for OIDC users
- Add auth_provider field to UserDto, derived from oidc_provider
  ("local" for password users, provider name for OIDC users)
- Block change_password() for OIDC users with clear error message
- Block admin_reset_password() for OIDC users

Fixes #122, Fixes #123

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-21 20:26:21 +01:00
Jan Wiebe 4e2c9d2592 feat(wopi): add WOPI protocol support for collaborative editing
Implement the Web Application Open Platform Interface (WOPI) protocol
to enable collaborative document editing with Collabora Online and
OnlyOffice through OxiCloud.

Backend:
- WOPI token service with HMAC-SHA256 signed access tokens
- WOPI lock service with in-memory lock management and expiry
- WOPI discovery service for auto-detecting editor capabilities
- WOPI HTTP handler: CheckFileInfo, GetFile, PutFile, Lock/Unlock
- File entity extended with owner_id for WOPI file-info responses
- Configuration via WOPI_* environment variables
- Services wired through DI in AppState

Frontend:
- WOPI editor component with modal and new-tab viewing modes
- Context menu integration for opening files in online editors
- Inline viewer integration for document preview

Infrastructure:
- Docker Compose file for local Collabora/OnlyOffice dev setup

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-21 13:39:27 +01:00
Jan Wiebe 807370e194 style: apply cargo fmt formatting to existing codebase
Run `cargo fmt` across all Rust source files to enforce consistent
formatting (import ordering, line wrapping, match arm braces).

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-21 13:33:18 +01:00
George Wu 5738ddbb54 fix(admin): populate OIDC settings in admin panel from environment variables
- Use load_effective_oidc_config() to get OIDC settings, which applies env var overrides
- Use effective.redirect_uri for callback_url instead of calculated value from server_base_url
- This ensures the admin panel shows the correct OXICLOUD_OIDC_REDIRECT_URI value
2026-02-18 19:41:43 -08:00
George Wu e2a2026c3d fix(admin): populate OIDC settings in admin panel from environment variables
The get_oidc_settings() method was reading directly from the database only,
without applying environment variable overrides. Now it uses load_effective_oidc_config()
which properly applies env var overrides (OXICLOUD_OIDC_*), ensuring the admin
panel accurately reflects the runtime configuration.
2026-02-18 19:25:53 -08:00
Diocrafts 0d7c8bc019 perf: optimize UI rendering and event lifecycle 2026-02-18 10:53:43 +01:00
Dionisio f70890e884 fix: upload failure, share dialog, shared view styling, dark mode fixes
- Fix 'folder_id is required' upload error by adding create_home_folder
  through the full hexagonal architecture (trait, service, repository, auth)
- Fix double upload issue with _isUploading concurrency guard
- Fix Share context menu doing nothing (ID collision between sharedView
  and main share dialog resolved with sv- prefix)
- Fix Compartidos tab duplicate headers and broken layout
- Add missing .shared-dialog CSS with dark mode support
- Fix dark mode white backgrounds on empty-state, shared-filters,
  trash-actions, action-btn, and header
- Fix i18n key mismatches in sharedView
- Bump version to 0.4.1

Closes #120
2026-02-16 16:18:39 +01:00
Dionisio b7bd656a43 perf: fix issues #1,#2,#3,#13 from performance audit
- #1  image_transcode: dedicated rayon pool + moka cache (no tokio blocking)
- #2  ltree materialized paths: eliminate N+1 folder/file queries
- #3  compute_content_hash: read blob_hash column instead of loading file into RAM
- #13 event delegation + DocumentFragment: ~15 delegated listeners replace ~19k per-item listeners

Backend: 82/82 tests pass, cargo check clean.
Frontend: ui.js and app.js syntax-validated via Node.js.
2026-02-16 13:10:44 +01:00
Dionisio d6c4eb884d feat: batch favorites endpoint + frontend dedup fixes
Backend:
- Add POST /api/favorites/batch endpoint (single multi-row INSERT)
- Add BatchFavoritesResult/BatchFavoritesStats DTOs
- Add batch methods to ports, service, PG repository
- Transaction-based insert with ON CONFLICT DO NOTHING, chunking at 5000

Frontend:
- Rewrite batchFavorites() to single API call (40 requests → 1)
- Add _replaceCacheFromResponse() to avoid extra GET round-trip
- Centralize formatFileSize, isTextViewable, formatDateTime, formatDateShort
- Remove duplicate icon mapping from app.js
- Fix inconsistent quota defaults (10GB everywhere)
2026-02-16 09:17:54 +01:00
Dionisio fb652c07e3 feat(P1+P2): server-authoritative favorites/recent + pre-computed display fields
P1-A: Enrich FavoriteItemDto & RecentItemDto with item_name, item_size,
      item_mime_type, parent_id, modified_at via SQL LEFT JOINs — eliminates
      N+1 per-item fetches.

P1-B: Rewrite favorites.js as server-authoritative (724→380 lines).
      In-memory cache backed by GET /api/favorites; no localStorage.

P1-C: Rewrite recent.js as server-authoritative (341→249 lines).
      GET /api/recent + POST /api/recent/{type}/{id}; no localStorage.

P1 cleanup: Remove dead localStorage cleanup from auth.js logout,
            fix async clearRecentFiles in app.js.

P2: Add icon_class, icon_special_class, category, size_formatted to
    FileDto, FolderDto, FavoriteItemDto, RecentItemDto. New shared
    display_helpers.rs module centralises mime→icon/category/size logic.
    Frontend (ui.js, fileRenderer.js, favorites.js, recent.js) now reads
    pre-computed fields from the API with fallback defaults — eliminates
    5 duplicated mime→icon mapping blocks (~89 lines removed).

Also fixes: synthetic FolderDto in webdav_handler.rs, pre-existing
            missing item_name field in share_service.rs test.

Net: -307 lines across 15 files. cargo check: 0 errors, 0 warnings.
     cargo test display_helpers: 3/3 pass.
2026-02-16 01:09:28 +01:00
Dionisio 5a679dfc90 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
2026-02-16 00:22:42 +01:00