diff --git a/CLAUDE.md b/CLAUDE.md index b8b2c4b..972027e 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -4,7 +4,7 @@ DocuVault is a multi-user SaaS document management platform built on FastAPI (Python) + Vue 3. It handles document upload, text extraction (PDF/DOCX/image/text), AI-based topic classification, per-user isolated storage, folder organization, document sharing, and pluggable cloud storage backends (OneDrive, Google Drive, Nextcloud, WebDAV). -**Current state:** v0.2.1 — Phase 12 gap-closure complete 2026-06-20. Migration-gated Compose startup (migrate service, service_completed_successfully dependency gate), 0005→0006 upgrade regression tests, RUNBOOK migration ops section. Provider-neutral cloud resource contract (CloudResourceAdapter, CloudResource, CloudCapability), durable CloudItem/CloudFolderState metadata layer (Alembic migration 0006), connection-ID browse API with IDOR protection and stale-while-revalidate, refresh_cloud_folder Celery task, and capability-aware StorageBrowser frontend. 12 of 12 phases complete. Not cleared for public internet deployment. +**Current state:** v0.2.3 — Phase 12.1 Plan 01 complete 2026-06-22. Cloud provider adapter contract repaired: Nextcloud now inherits canonical `WebDAVBackend.list_folder(connection_id, user_id, parent_ref=None, page_token=None) -> CloudListing`; legacy `list_folder(folder_path="") -> list[dict]` override removed. `normalize_nextcloud_url()` added to `cloud_utils.py`. OneDrive `@odata.nextLink` cross-origin guard added. Four-provider parametrized contract suite in `test_cloud_provider_contract.py`. Phase 12.1 in progress. Not cleared for public internet deployment. ## Stack @@ -41,12 +41,14 @@ Before adding a helper, check if it belongs in an existing shared module: | `backend/ai/utils.py` | `strip_code_fences`, `parse_classification`, `parse_suggestions` — AI response parsing shared by all providers | | `backend/services/auth.py` | `validate_password_strength(password)` — raises `ValueError`; routers catch and re-raise as `HTTPException` | | `backend/storage/cloud_base.py` | `CloudResourceAdapter`, `CloudListing`, `CloudResource`, `CloudCapability` — Phase 12 browse contract; all 4 providers implement this | +| `backend/storage/cloud_utils.py` | `validate_cloud_url`, `normalize_nextcloud_url`, `encrypt_credentials`, `decrypt_credentials` — Phase 12.1 adds `normalize_nextcloud_url` | | `backend/services/cloud_items.py` | `resolve_owned_connection`, `upsert_cloud_item`, `reconcile_cloud_listing`, `get_or_create_folder_state` — owner-scoped cloud metadata service | | `backend/api/cloud/schemas.py` | `CloudBrowseResponse`, `CloudItemOut`, `FolderFreshnessOut`, `ConnectionRenameRequest` — credential-free cloud response schemas | **Rules:** - No router may define `_ip()`, `_get_ip()`, or any other local variant of `get_client_ip`. Import from `deps.utils`. - No router may define its own `CloudConnectionError`. Import from `storage.exceptions`. +- `NextcloudBackend` must NOT override `list_folder` — it inherits the canonical `WebDAVBackend` implementation. Any Nextcloud-specific URL handling belongs in `normalize_nextcloud_url` in `cloud_utils.py`. - No AI provider may define its own `_strip_code_fences` or `_parse_*`. Import from `ai.utils`. - No API file may define `_validate_password_strength`. Import from `services.auth`. - Service layer raises `ValueError` (or domain exceptions), never `HTTPException`. Only the router layer raises `HTTPException`. diff --git a/README.md b/README.md index b386d1e..b1067c8 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # DocuVault -**Version 0.2.1 — Alpha** +**Version 0.2.3 — Alpha** > **Not production-ready.** DocuVault is functional for local and self-hosted use but has not been audited or hardened for public internet exposure. APIs, environment variables, and the database schema may change without notice until a stable 1.0 release is declared. @@ -303,11 +303,13 @@ Users connect cloud storage through **Settings → Cloud Storage**. Credentials |----------|-------------|-------| | Google Drive | OAuth 2.0 | Requires `GOOGLE_CLIENT_ID/SECRET` | | Microsoft OneDrive | OAuth 2.0 (MSAL) | Requires `ONEDRIVE_CLIENT_ID/SECRET` | -| Nextcloud | Username + password | Custom server URL; SSRF allowlist enforced | +| Nextcloud | Username + password | Custom server URL; SSRF allowlist enforced; URL normalized to canonical DAV root | | WebDAV | Username + password | Any RFC 4918 server; SSRF allowlist enforced | Connection statuses: `ACTIVE`, `REQUIRES_REAUTH`, `ERROR`. An externally revoked OAuth token transitions to `REQUIRES_REAUTH` without a 500 error. +All four providers implement the canonical `CloudResourceAdapter.list_folder(connection_id, user_id, parent_ref=None, page_token=None) -> CloudListing` contract. `@odata.nextLink` pagination for OneDrive is validated to stay on `graph.microsoft.com` before following. + ### Connection-ID Browse API (Phase 12) Each connected account is independently addressable by its connection UUID: diff --git a/backend/main.py b/backend/main.py index e45f5f6..7b3d690 100644 --- a/backend/main.py +++ b/backend/main.py @@ -244,7 +244,7 @@ async def lifespan(app: FastAPI): # ── Application factory ─────────────────────────────────────────────────────── -app = FastAPI(title="Document Scanner API", version="0.2.2", lifespan=lifespan) +app = FastAPI(title="Document Scanner API", version="0.2.3", lifespan=lifespan) # Rate limiter state (slowapi) app.state.limiter = auth_limiter diff --git a/backend/storage/nextcloud_backend.py b/backend/storage/nextcloud_backend.py index 4296f86..593dc2f 100644 --- a/backend/storage/nextcloud_backend.py +++ b/backend/storage/nextcloud_backend.py @@ -29,6 +29,8 @@ Credentials dict shape (same as WebDAVBackend): """ from __future__ import annotations +import asyncio + from storage.cloud_utils import validate_cloud_url from storage.webdav_backend import WebDAVBackend diff --git a/frontend/package.json b/frontend/package.json index 41df90d..862abd8 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -1,6 +1,6 @@ { "name": "document-scanner-frontend", - "version": "0.2.2", + "version": "0.2.3", "type": "module", "scripts": { "dev": "vite",