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
* 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);
}
```
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
Security: session hardening
Refresh token rotation with theft detection (family_id)
- Added family_id column to auth.sessions (migration 20260507000000_session_family.sql) grouping all tokens issued from the same login into a family
- On refresh, the new session inherits the parent's family_id
- If a revoked token is replayed (indicates the token was stolen after rotation), the entire family is immediately invalidated and a warning is logged — forcing re-authentication on all devices
SameSite=Strict on refresh cookie
- Access cookie stays SameSite=Lax (needed for top-level navigation)
- Refresh cookie upgraded to SameSite=Strict — it is only ever used for explicit POST to /api/auth/refresh, never via cross-site navigation
Refresh token TTL: 30 days → 7 days
- With rotation, active sessions auto-renew and effectively never expire
- Inactive sessions expire after 7 days instead of 30, reducing the theft window
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.
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
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
The constant 10 * 1024 * 1024 * 1024 (10 GB) overflows on 32-bit systems
where usize is 32-bit (max ~4GB). This caused compilation failures on
ARMv7 architecture.
Fix by using architecture-appropriate limits:
- 64-bit: 10 GB (unchanged)
- 32-bit: 1 GB (safe maximum for 32-bit usize)
Fixes#206
The Nextcloud Android client compares ETags before and after upload to
verify its write landed. OxiCloud was returning the stable file UUID as
the ETag, which never changed on content updates, causing false
SYNC_CONFLICT errors on every upload.
Five fixes applied:
1. Thread blob_hash (SHA-256) through File entity, FileDto, all read/write
queries, and all WebDAV/PROPFIND responses as the ETag — changes on
every content update, no DB migration needed.
2. Honor X-OC-Mtime header: parse the client-supplied mtime and use it
for updated_at via COALESCE(to_timestamp($n), NOW()).
3. Disable phantom checksum capability (preferredUploadType/supportedTypes)
that the server never actually implemented, stopping retry loops.
4. Add nc:creation_time and nc:upload_time to PROPFIND responses.
5. Return oc-etag header in chunked upload MOVE (assemble) responses.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
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.
- 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
Nextcloud app uploads sent application/octet-stream as Content-Type,
causing images to not be recognized. Now both WebDAV PUT and chunked
upload paths call refine_content_type() which detects via magic bytes,
then extension, then falls back to the client header. Also fixes
update_file() which previously hardcoded application/octet-stream.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Move pg_trgm extension creation before CalDAV indexes that depend on it
- Remove duplicate app-password route registration that caused panic
- Fix missing comma in language selector array (Dutch entry)
- Await async canEdit() in file click handler (Promise was always truthy)
- Detect images by extension fallback when mime_type is octet-stream
(files uploaded via Nextcloud WebDAV API lack correct mime types)
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
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>
- 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