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:
@@ -93,6 +93,9 @@ pub trait UserStoragePort: Send + Sync + 'static {
|
||||
/// Lists users with pagination
|
||||
async fn list_users(&self, limit: i64, offset: i64) -> Result<Vec<User>, DomainError>;
|
||||
|
||||
/// Searches users by username or email (SQL ILIKE) with a limit.
|
||||
async fn search_users(&self, query: &str, limit: i64) -> Result<Vec<User>, DomainError>;
|
||||
|
||||
/// Lists users by role (e.g., "admin" or "user")
|
||||
async fn list_users_by_role(&self, role: &str) -> Result<Vec<User>, DomainError>;
|
||||
|
||||
@@ -249,8 +252,19 @@ pub trait AppPasswordStoragePort: Send + Sync + 'static {
|
||||
/// Update the `last_used_at` timestamp after a successful authentication.
|
||||
async fn touch_last_used(&self, id: &str) -> Result<(), DomainError>;
|
||||
|
||||
/// Deactivate (soft-delete) an app password.
|
||||
async fn revoke(&self, id: &str) -> Result<(), DomainError>;
|
||||
/// Get active app passwords for a user filtered by token prefix (first 8 chars).
|
||||
/// More efficient than `get_active_by_user_id` when the password prefix is known.
|
||||
async fn get_active_by_user_prefix(
|
||||
&self,
|
||||
user_id: &str,
|
||||
prefix: &str,
|
||||
) -> Result<Vec<AppPassword>, DomainError>;
|
||||
|
||||
/// Deactivate (soft-delete) an app password, scoped to the owning user.
|
||||
async fn revoke(&self, id: &str, user_id: &str) -> Result<(), DomainError>;
|
||||
|
||||
/// Delete an app password owned by a specific user. Returns true if found and deleted.
|
||||
async fn delete_by_user_and_id(&self, id: &str, user_id: &str) -> Result<bool, DomainError>;
|
||||
|
||||
/// Hard-delete expired/revoked app passwords (cleanup).
|
||||
async fn delete_expired(&self) -> Result<u64, DomainError>;
|
||||
|
||||
@@ -152,6 +152,12 @@ pub trait DedupPort: Send + Sync + 'static {
|
||||
/// Calculate BLAKE3 hash of a file (streaming).
|
||||
async fn hash_file(&self, path: &Path) -> Result<String, DomainError>;
|
||||
|
||||
/// Get the physical filesystem path for a blob by its hash.
|
||||
///
|
||||
/// Returns the path where the blob is stored on disk.
|
||||
/// Used by services that need direct filesystem access (e.g., thumbnail generation).
|
||||
fn blob_path(&self, hash: &str) -> PathBuf;
|
||||
|
||||
/// Get deduplication statistics.
|
||||
async fn get_stats(&self) -> DedupStatsDto;
|
||||
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
use std::collections::HashSet;
|
||||
|
||||
use crate::application::dtos::favorites_dto::{BatchFavoritesResult, FavoriteItemDto};
|
||||
use crate::common::errors::Result;
|
||||
|
||||
@@ -27,6 +29,14 @@ pub trait FavoritesUseCase: Send + Sync {
|
||||
user_id: &str,
|
||||
items: &[(String, String)],
|
||||
) -> Result<BatchFavoritesResult>;
|
||||
|
||||
/// Check which of the given item IDs are favorites for this user.
|
||||
/// Returns the set of item_ids that are favorites.
|
||||
async fn batch_check_favorites(
|
||||
&self,
|
||||
user_id: &str,
|
||||
item_ids: &[(&str, &str)], // (item_id, item_type) pairs
|
||||
) -> Result<HashSet<String>>;
|
||||
}
|
||||
|
||||
// ─────────────────────────────────────────────────────
|
||||
@@ -54,4 +64,12 @@ pub trait FavoritesRepositoryPort: Send + Sync + 'static {
|
||||
/// Insert multiple items in a single transaction.
|
||||
/// Returns the number of rows actually inserted (ignoring duplicates).
|
||||
async fn add_favorites_batch(&self, user_id: &str, items: &[(String, String)]) -> Result<u64>;
|
||||
|
||||
/// Check which of the given item IDs are favorites for this user.
|
||||
/// Returns the set of item_ids that are favorites.
|
||||
async fn batch_check_favorites(
|
||||
&self,
|
||||
user_id: &str,
|
||||
item_ids: &[(&str, &str)], // (item_id, item_type) pairs
|
||||
) -> Result<HashSet<String>>;
|
||||
}
|
||||
|
||||
@@ -82,6 +82,9 @@ pub trait FileReadPort: Send + Sync + 'static {
|
||||
/// Gets the parent folder ID from a path (WebDAV).
|
||||
async fn get_parent_folder_id(&self, path: &str) -> Result<String, DomainError>;
|
||||
|
||||
/// Gets a folder ID by its path.
|
||||
async fn get_folder_id_by_path(&self, folder_path: &str) -> Result<String, DomainError>;
|
||||
|
||||
/// Gets the content-addressable blob hash for a file (O(1) DB lookup).
|
||||
///
|
||||
/// Returns the BLAKE3 hash stored in `storage.files.blob_hash`.
|
||||
|
||||
Reference in New Issue
Block a user