- SMTP has a mock to enable end to end test and validate the whole path
(via OXICLOUD_SMTP_MOCK)
- add email normalisation ( including punicode)
- api to share to external user
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
- Add src/application/dtos/cursor.rs with three shared types:
· PageCursor trait — default base64url+JSON encode/decode; one bare
impl line per cursor struct
· CursorQuery struct — standard limit/cursor/sort_by query params with
limit_clamped() and decode_cursor<C>() helpers; compose via flatten
· CursorListResponse<T> — standard {items, next_cursor?} envelope with
from_oversized() and with_cursor() builders
- Migrate GrantCursor to impl PageCursor (remove duplicate encode/decode)
- Update GET /api/grants/incoming/resources:
· SharedWithMeQuery now embeds CursorQuery via #[serde(flatten)]
· Replace file/folder nullable pair with ResourceContentDto (untagged
enum) under a single always-present 'resource' field
· SharedWithMeDto is now a type alias for CursorListResponse<SharedWithMeItemDto>
· Handler uses q.paging.limit_clamped() and decode_cursor<GrantCursor>()
- Add docs/architecture/resource-listing.md — authoritative contract for
all listing endpoints (cursor design, SQL keyset WHERE, sort_by naming,
Rust + JS skeletons, compliance table, migration guide)
- Register doc in VitePress sidebar and architecture index
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* 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);
}
```
- GET /api/s/{token}/contents — list root folder of a shared folder
- GET /api/s/{token}/contents/{folder_id} — list subfolder
- GET /api/s/{token}/file/{file_id} — download a file within a shared folder
- GET /api/s/{token}/zip — download root as ZIP
- GET /api/s/{token}/zip/{folder_id} — download subfolder as ZIP
(added from @abnvle)
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