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>
This commit is contained in:
zjean
2026-03-04 14:02:15 +01:00
parent ecd1a8148a
commit 54eedf5483
64 changed files with 6761 additions and 126 deletions
@@ -120,6 +120,62 @@ pub enum LockType {
Write,
}
/// Extra property context for Nextcloud/ownCloud WebDAV extensions.
#[derive(Debug, Clone)]
pub struct NextcloudPropContext {
pub file_id: Option<i64>,
pub oc_id: Option<String>,
pub owner_id: Option<String>,
pub owner_display_name: Option<String>,
pub permissions: String,
pub size: u64,
pub has_preview: bool,
pub is_encrypted: bool,
pub mount_type: String,
pub contained_file_count: u64,
pub contained_folder_count: u64,
}
impl NextcloudPropContext {
pub fn for_folder(
file_id: Option<i64>,
oc_id: Option<String>,
owner: &str,
contained_files: u64,
contained_folders: u64,
) -> Self {
Self {
file_id,
oc_id,
owner_id: Some(owner.to_string()),
owner_display_name: Some(owner.to_string()),
permissions: "RGDNVCK".to_string(),
size: 0,
has_preview: false,
is_encrypted: false,
mount_type: "dir".to_string(),
contained_file_count: contained_files,
contained_folder_count: contained_folders,
}
}
pub fn for_file(file_id: Option<i64>, oc_id: Option<String>, owner: &str, size: u64) -> Self {
Self {
file_id,
oc_id,
owner_id: Some(owner.to_string()),
owner_display_name: Some(owner.to_string()),
permissions: "RGDNVW".to_string(),
size,
has_preview: false,
is_encrypted: false,
mount_type: "file".to_string(),
contained_file_count: 0,
contained_folder_count: 0,
}
}
}
/// WebDAV adapter for converting between XML and domain objects
pub struct WebDavAdapter;