Commit Graph

77 Commits

Author SHA1 Message Date
Edouard Vanbelle d81f5dbe48 feat(userLifecycle): migrate create_personal_folder() use now on_user_login on_user_created (only if user is not external) 2026-06-01 22:51:57 +02:00
Edouard Vanbelle bb6429a620 refactor(userLifecycle): add user lifecycle, more clarety + better integration for the future 2026-06-01 22:51:53 +02:00
Edouard Vanbelle 09985f8a95 feat(group): 1st implementation of Groups
this implements first version (manageable only by admin right now)

    routes:

        GET /api/groups
        List subject groups (paginated). Admin-only.

        POST /api/groups
        Create a new ReBAC subject group. Admin-only. The name must match the RFC 5321 local-part shape and be globally unique (case-insensitive).

        GET /api/groups/search
        Search non-virtual groups by name substring. Authenticated only (no admin role required) — backs the share-dialog recipient autocomplete.

        GET /api/groups/{id}
        Fetch a single group's details. Admin-only.

        DELETE /api/groups/{id}
        Delete a group. Cascades to `subject_group_members` (FK) and to `access_grants` rows referencing this group as a subject. Admin-only.

        PATCH /api/groups/{id}
        Update a group's metadata. Admin-only. v1 only persists name renames.

        GET /api/groups/{id}/effective-members
        List every user transitively reached through this group (members of members of members, etc.). Used by admin / audit tooling. Admin-only.

        GET /api/groups/{id}/members
        List the *direct* members of a group (one level only). Admin-only.

        POST /api/groups/{id}/members
        Add a member to a group. Exactly one of `user_id` / `group_id` must be provided. Adding a group-member runs a write-time cycle check and a nesting-depth check (max 8). Admin-only.

        DELETE /api/groups/{id}/members/group/{gid}
        Remove a nested group-member from a group. Admin-only.

        DELETE /api/groups/{id}/members/user/{uid}
        Remove a user-member from a group. Admin-only.

fix hurl

groups

round

groups
2026-05-31 20:57:45 +02:00
Edouard Vanbelle 79c1a37931 feat(ui): 1 modal to manage shares (users & public share)
fix(share): ensure Authz parent is created/updated on publicShare create/update

fix(ShareModal): do not show Token (public) grants in People section
2026-05-25 22:47:36 +02:00
Edouard Vanbelle 73f0b0fa47 refactor(lifecycle hooks): simplify integration of new services
* make more coherent lifecycles
  * remove specific implementation on different handlers (they do not need to know existence of ThumbnailSerice nor AudioMetadataService)
  * reduce risk of orphean objects
  * ensure additional services are correctly wired (ex: Thumbnail generation was not covering all upload cases)
  * more details on docs/architecture/file-and-blob-lifecycle.md :

```rust
// application/ports/file_lifecycle.rs
pub trait FileLifecycleHook {

    fn on_file_created(file_id, blob_hash, content_type, is_new_blob);
    fn on_file_updated(file_id, blob_hash, content_type);
    fn on_file_copied(file_id, blob_hash, content_type, source_id)
    fn on_file_deleted(file_id);
}

// application/ports/blob_lifecycle.rs
pub trait BlobLifecycleHook {

    fn on_blob_created(blob_hash, content_type);
    fn on_blob_deleted(blob_hash);
}
```
2026-05-22 13:40:58 +02:00
Edouard Vanbelle 76a85949e7 fix(trash)+refactor(file life cycle)
* fix issue with the empty trash (wasn't calling thumbnail clean up)
 * refactor file service life cycle (TrashService don't call directly ThumbnailService, but call the on_file_deleted() hook
 * remove unused mehod: _validate_user_ownership()
2026-05-22 00:28:39 +02:00
Edouard Vanbelle 3362e277ab feat(authz): check permission on read handlers + check create permission on folder 2026-05-21 20:30:53 +02:00
Edouard Vanbelle cba9be8c21 feat(rebac): first pass 2026-05-20 22:56:00 +02:00
Edouard Vanbelle a38475bd2c style(server): align text 2026-05-19 09:42:44 +02:00
Dionisio Pozo 1e34c5f3f8 Merge pull request #330 from EdouardVanbelle/fix/thumbnail-on-update
bugfix/thumbnails on update
2026-05-14 00:38:03 +02:00
Edouard Vanbelle d85b8055b8 refactor(services): add file_lifecycle and blob_lifecycle
- complete src/application/ports/blob_lifecycle.rs with traits:
    * BlobCreationHook
    * BlobDeletionHook

 - add src/application/ports/file_lifecycle.rs with traits:
    * FileCreatedHook
    * FileDeletedHook
    * FileUpdatedHook
2026-05-14 00:18:12 +02:00
Edouard Vanbelle 28e25f9d16 fix+test: check hash ref count on copy-on-write (a duplicated beeing updated) 2026-05-13 15:55:41 +02:00
Edouard Vanbelle 78cb37b311 feat: check thumbnail cleanup on files deletion + correct ref counter 2026-05-13 15:55:38 +02:00
Edouard Vanbelle 9f57776ec9 bugfix/thumbnails on update
Bug 1 & 2 (webdav_handler.rs handle_put() update branch):
  - After a successful file update via WebDAV PUT, if the content type is a supported image:
    a. delete_thumbnails(file_id) — evicts the stale moka cache entry
    b. Spawns a background task to read the new blob bytes and call generate_all_sizes_background_from_bytes

  Bug 3 & 4 (dedup_service.rs):
  - Added thumbnail_service: Option<Arc<ThumbnailService>> field with a with_thumbnail_service() builder
  - In remove_legacy_reference(): calls delete_blob_thumbnails(hash) when ref_count hits 0
  - In remove_manifest_reference(): calls delete_blob_thumbnails(file_hash) when manifest's last ref is dropped
  - Wired in di.rs — the thumbnail service is created before dedup service so the ordering works cleanly
2026-05-12 09:00:26 +02:00
abnvle d15d7b8f8e feat(share): public folder browsing API + range support + zip
Five new public endpoints under /api/s/{token}/...:
  GET /contents
  GET /contents/{folder_id}
  GET /file/{file_id}
  GET /zip
  GET /zip/{folder_id}

All honour the unlock cookie from /verify, so password-protected
folder shares work end-to-end.

Folder/file IDs are validated against the share subtree via a single
ltree containment query (O(log N) on the existing GiST index).
Out-of-scope IDs return 404.

download_shared_file refactored to a Range/304/206/416-aware
serve_share_file helper, shared with the new /file/{file_id}
endpoint. content_disposition extracted from FileHandler so RFC 5987
formatting is identical across auth and share download paths.
2026-05-05 22:41:55 +02:00
Diocrafts cd3733b459 feat: pluggable storage backends (S3, Azure, local) with admin UI
Implement 4-phase external storage backends architecture:

Phase 1 - Foundation:
- BlobStorageBackend trait (application/ports/blob_storage_ports.rs)
- LocalBlobBackend: extracted all tokio::fs ops from DedupService
- S3BlobBackend: AWS SDK with custom endpoint support (MinIO, R2, B2)
- DedupService refactored to use Arc<dyn BlobStorageBackend>

Phase 2 - Admin Panel:
- StorageSettingsService with DB persistence + env override
- Storage tab in admin panel (backend selector, S3 form, provider presets)
- GET/PUT/POST endpoints for storage settings + connection test
- i18n keys (en/es) and BEM CSS

Phase 3 - Migration:
- MigrationBlobBackend decorator (dual-read: target-first + source fallback)
- Background migration job with parallel transfers + progress tracking
- Migration UI (progress bar, ETA, pause/resume/verify/complete)
- 6 admin API endpoints for migration lifecycle

Phase 4 - Enterprise Extras:
- CachedBlobBackend: LRU disk cache for remote backends
- EncryptedBlobBackend: AES-256-GCM at-rest encryption
- AzureBlobBackend: Azure Blob Storage support
- RetryBlobBackend: exponential backoff for transient errors
- Decorator composition in DI: retry → encryption → cache

All 223 tests passing, clippy clean, fmt verified.
2026-04-14 21:33:38 +02:00
Diocrafts d32a32c30a perf: add TTL/TTI to content cache and invalidate on file mutations 2026-04-11 17:39:17 +02:00
Andrey Tkachenko da066f47fa Music Player & Playlist Manager 2026-04-08 15:14:03 +03:00
Edouard Vanbelle badf35f08f chore: remove all executable attributes on non bash files 2026-04-01 23:14:42 +02:00
Claude dacb8f388e fix(thumbnail): add timeout protection for large image processing
Resolves conflict between main and PR #229 by applying timeout support
on top of main's drop(data) memory optimization. Changes:
- Add thumbnail_generation_ms to TimeoutConfig (default 30s)
- Add generation_timeout field to ThumbnailService
- Wrap spawn_blocking in tokio::time::timeout to prevent hanging
- Update DI to pass timeout from config
- Update tests to provide timeout parameter

https://claude.ai/code/session_015FD62aAoMYx1XBZbzPvUu8
2026-03-28 18:40:34 +00:00
BillionClaw 30d6e53fca fix(file): verify target folder ownership on move
When moving a file to a folder, verify that the caller owns the target
folder. Without this check, a file could be moved to another user's
folder, causing the file to "disappear" from the original user's view
since file listings filter by user_id.

- Add folder_repo to FileManagementService
- Add verify_target_folder_owner() method
- Call it in move_file_owned() before moving
2026-03-17 16:53:55 +08:00
Diocrafts 661c9cb688 fix: delete thumbnails when files are permanently deleted
- Inject ThumbnailService into TrashService and FileManagementService
- Call delete_thumbnails() after permanent file deletion in:
  - TrashService::delete_permanently (single item)
  - TrashService::empty_trash (bulk: collects file IDs first)
  - FileManagementService::delete_file
  - FileManagementService::delete_with_cleanup (fallback path)
- All thumbnail cleanup is best-effort (warn on failure, never blocks)
- Prevents orphaned thumbnail files from accumulating on disk
2026-03-07 19:15:36 +01:00
Jared Wolff 6a84a5c44e fix(photos): fix SQL type mismatch, pagination panic, missing icons, and add day/month/year grouping
- Fix photos endpoint 500: remove ::uuid cast on user_id WHERE clause (VARCHAR column)
- Fix pagination underflow panic when total_pages is 0
- Add missing 'images' and 'play' icons to SVG icon registry
- Add day/month/year grouping toggle with localStorage persistence
- Improve grid spacing and group header styling per mode
- Add i18n translations for grouping labels (all 9 locales)
2026-03-05 17:32:28 -05:00
Jared Wolff 69fe3a8b07 feat(photos): add EXIF metadata extraction and storage
Extract EXIF orientation, GPS coordinates, camera info, and timestamps
from uploaded images using kamadak-exif. Store metadata in a new
file_metadata PG table. Apply EXIF orientation to thumbnail generation
so images display correctly. Add /api/files/{id}/metadata endpoint.
2026-03-05 17:32:28 -05:00
Jared Wolff 4f7e79f605 style: collapse nested if-let in JWT secret config (clippy) 2026-03-05 16:37:47 -05:00
Dionisio f2d35ca792 feat: auto-persist JWT secret, remove setup token requirement
- JWT secret auto-generates and persists to <STORAGE_PATH>/.jwt_secret
- Remove setup token: first admin setup is open until system initialized
- Fix schema.sql: move CREATE EXTENSION pg_trgm/ltree to top
- Update login UI and auth.js to remove setup token fields
2026-03-05 22:12:53 +01:00
zjean 54eedf5483 feat(nextcloud): add Nextcloud-compatible API layer
Implement a complete Nextcloud client compatibility layer so that
Nextcloud desktop/mobile sync clients can connect to OxiCloud.

Key additions:
- Login Flow v2 (device auth) with OIDC bridge support
- WebDAV handler compatible with Nextcloud clients (PROPFIND, GET,
  PUT, DELETE, MKCOL, MOVE, COPY, HEAD, PROPPATCH)
- OCS API endpoints (user info, capabilities, notifications stubs,
  sharees, unified search)
- Basic Auth middleware with app password verification, account
  lockout integration, and blake3-keyed auth cache
- App password management: create, list, revoke via both native
  API (JWT-authenticated profile page) and Nextcloud OCS endpoints
- Nextcloud file ID mapping (oc:fileid) with persistent DB storage
- Chunked upload support (Nextcloud v2 chunking protocol)
- Trashbin WebDAV interface
- Avatar (SVG placeholder) and preview (redirect) handlers
- User profile page with app password management UI
- URL user validation on all DAV routes (403 on mismatch)
- Database schema for app_passwords and nextcloud_object_ids tables

All services are behind a `nextcloud.enabled` config flag and
cleanly separated under src/interfaces/nextcloud/.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-05 20:46:07 +01:00
Diocrafts ee86c3a128 fix: resolve all clippy warnings and convert integration_tests to custom cfg
- Add type aliases (FileRow, FolderRow, FolderRowPaginated, FolderRowOptUser) to reduce type complexity
- Simplify redundant closures in app_password_handler and webdav_handler
- Remove needless borrow in auth_handler
- Collapse nested if/let chains in login_lockout, webdav_lock, auth, rate_limit
- Box LockEntry in acquire() Err variant to fix large enum variant warning
- Rename DeviceCodeStatus::from_str to parse to avoid should_implement_trait lint
- Add #[allow(clippy::too_many_arguments)] and #[allow(clippy::result_unit_err)] where appropriate
- Convert integration_tests from cargo feature to custom cfg attribute
- Add check-cfg lint config in Cargo.toml for integration_tests cfg
2026-03-04 23:55:08 +01:00
Dionisio 4a60fdc984 fix(security): IDOR protection for file operations
Adds ownership verification at repository, service, and handler layers
for download, rename, move, and delete file operations.

- Repository: get_file_for_owner() with AND user_id= SQL filter
- Service: _owned() methods with verify_owner() fail-closed guard
- Handlers: require AuthUser, delegate to _owned() methods
- Tests: 10 IDOR protection tests (all passing)
- Cleanup: remove dead OptionalUserId import, gate broken pre-existing
  test modules behind integration_tests feature flag
2026-03-04 17:18:39 +01:00
Dionisio 98fb3e6408 fix(security): VULN-01 admin escalation + VULN-02 path traversal hardening
VULN-01 - Admin privilege escalation:
- Harden register() to reject is_admin=true
- Add /api/setup endpoint with setup_token for initial admin creation
- Add SetupAdminDto and setup_token to AppState
- Remove dead code from auth handler

VULN-02 - Path traversal (CVSS ~8.6):
- Solution A+E: Harden StoragePath constructors (from_string, new, join)
  to strip '..' and '.' segments and reject slash injection
- Solution B: resolve_path() now returns Result<PathBuf>, calls
  validate_path() internally, and verifies resolved path stays under root
- Update StoragePort trait signature to return Result<PathBuf, DomainError>
- Remove dead code: FilePathResolutionPort, StorageVerificationPort,
  DirectoryManagementPort (declared but never implemented)
- Add 17 security tests covering traversal attack vectors
2026-03-04 14:14:40 +01:00
Claude 4bee05c690 merge: resolve conflict in auth_factory.rs with origin/main
Keep custom Result type alias (replacing anyhow) from main while
preserving our removal of unused TokenServicePort import.

https://claude.ai/code/session_01EbAFEfyJNLRmJHmmYDX3Tt
2026-03-03 15:42:28 +00:00
Claude 1b49135ca9 perf: replace dyn trait objects with concrete types to eliminate vtable overhead
Remove async-trait dependency and use native Rust async fn in traits.
Replace Arc<dyn Trait> with Arc<ConcreteType> throughout the codebase
to enable monomorphization and eliminate dynamic dispatch overhead.

Key changes:
- Remove write-behind cache (no implementation existed)
- Fix should_transcode static method call
- Use ContactStorageAdapter directly instead of dyn AddressBookUseCase
- Clean up unused trait imports across services and DI

https://claude.ai/code/session_01EbAFEfyJNLRmJHmmYDX3Tt
2026-03-03 15:36:42 +00:00
Dionisio 6f30ac761f refactor: replace WebDAV LOCK stub with moka-backed lock store (auto-expire TTL) 2026-03-03 11:49:52 +01:00
Dionisio efcf88c4d7 style: cargo fmt --all 2026-03-03 01:49:18 +01:00
Dionisio 1df52fd702 security: add IP rate limiting + account lockout on auth endpoints
- Rate limit login (5/min), register (3/hr), refresh (10/min) per IP
- Account lockout after 5 consecutive failed logins (15 min cooldown)
- Fix stored XSS in admin panel (escapeHtml on all user-controlled data)
- All limits configurable via OXICLOUD_RATE_LIMIT_* / OXICLOUD_LOCKOUT_* env vars
- Zero new dependencies (uses existing moka crate for in-memory caches)
- Includes unit tests for lockout service
2026-03-03 01:44:39 +01:00
Dionisio b9bf7ba288 perf(webdav): single UNION ALL query for path resolution
Replace the double-query pattern (get_folder_by_path + get_file_by_path)
across PROPFIND, HEAD, DELETE, MOVE, and COPY handlers with a single
UNION ALL query via PathResolverService.

PG Append node short-circuits on LIMIT 1: if the folder branch matches,
the file branch is never executed. Cuts WebDAV path resolution from
2 round-trips to 1 per request.

Also adds an exists() method using EXISTS subqueries for the Overwrite
header checks in MOVE/COPY (avoids constructing full DTOs).

Legacy double-query fallback retained when PathResolver is unavailable.
2026-03-02 23:40:48 +01:00
Dionisio 81987e9321 fix: URL-decode DAV paths with spaces + feat: app passwords for Basic Auth
Bug fix:
- URL-decode paths in extract_webdav_path(), extract_caldav_path(),
  extract_carddav_path() so folders with spaces (e.g. 'My Folder') no
  longer return 404 when accessed via encoded URIs (%20)
- Properly encode href values in PROPFIND/PROPPATCH/LOCK XML responses
- Decode Destination header in MOVE/COPY operations

New feature - App Passwords (API keys for DAV clients):
- POST /api/auth/app-passwords  → create (shows token once)
- GET  /api/auth/app-passwords  → list (prefix only)
- DELETE /api/auth/app-passwords/:id → revoke
- Auth middleware now accepts both Bearer JWT and Basic Auth
- Argon2 hashed, scoped (webdav/caldav/carddav), optional expiry
- Compatible with DAVx5, Thunderbird, rclone, curl

Tested: 12/12 E2E tests pass (create, list, WebDAV/CalDAV/CardDAV
Basic Auth, URL-decode with spaces, wrong password 401, revoke, post-
revoke 401).
2026-03-01 20:34:12 +01:00
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 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 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 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 b0235e05c8 perf(issue#6): migrate ShareFsRepository to PostgreSQL
- Add storage.shares table with indexes on token, (item_id, item_type), created_by
- Create SharePgRepository with indexed SQL queries and window-function pagination
- Rewire DI to inject SharePgRepository with PgPool instead of config
- Delete legacy share_fs_repository.rs (295 lines of JSON file I/O)
- Remove dead module declaration from repositories/mod.rs

Eliminates O(n) full-file JSON reads/writes, TOCTOU races, and crash
corruption risk. All share operations now use indexed PG queries.
2026-02-24 10:09:49 +01:00
Diocrafts b48f2867ac security(P0): fail-closed when auth is enabled but cannot initialize
Previously, if enable_auth=true but the database or auth services failed
to initialize, the server silently started in PUBLIC mode — all API routes
accessible without authentication. This is a critical fail-open bug.

Fixes (fail-closed behavior):
1. main.rs: DB pool creation failure with enable_auth=true now panics
   instead of falling through with db_pool=None
2. di.rs: create_auth_services() failure now propagates the error via ?
   instead of logging and continuing with auth_services=None
3. main.rs: Added defensive assert! that auth_service is Some when
   enable_auth=true, preventing any future refactor from reintroducing
   the silent degradation

The server will now refuse to start if authentication is configured
but cannot be properly initialized.
2026-02-22 22:40:55 +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
Diocrafts fd9e509648 perf: implement findings #2, #3, #17 from architecture audit
- Finding #2: Replace Mutex<HashMap> with moka::sync::Cache in file_blob_read_repository (10K cap, 30s TTI)
- Finding #3: Add chunked upload persistence with session.json + progress.bin bitmask for crash recovery
- Finding #17: Remove manual gzip compression, delegate entirely to tower-http CompressionLayer
- Remove dead code: StubCompressionPort, GzipCompressionService re-export, duplicate response structs
- All 114 tests passing
2026-02-22 14:12:53 +01: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
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