280 Commits

Author SHA1 Message Date
curo1305 f509c37611 feat(05-05): register cloud and users routers in main.py
- Added import and app.include_router(cloud_router) for Phase 5 cloud endpoints
- Added app.include_router(cloud_users_router) for /api/users/me/default-storage
- Both routers registered after the Phase 4 audit router
- All 6 cloud routes + default-storage route visible in app.routes
2026-05-29 00:45:14 +02:00
curo1305 2424f52eee feat(05-05): implement cloud.py — all 7 cloud connection management endpoints
- GET /api/cloud/oauth/initiate/{provider}: generates state token (secrets.token_urlsafe(32)),
  stores in Redis with TTL 1800, redirects to Google Drive or OneDrive OAuth URL
- GET /api/cloud/oauth/callback/{provider}: validates state (single-use Redis key), exchanges
  code for tokens, encrypts credentials, upserts CloudConnection, audit log, redirects to
  {settings.frontend_url}/settings?cloud_connected={provider}; on error redirects with cloud_error=
- POST /api/cloud/connections/webdav: SSRF validates URL, tests health_check, encrypts creds,
  upserts CloudConnection, returns CloudConnectionOut (credentials_enc excluded)
- GET /api/cloud/connections: returns {"items": [CloudConnectionOut]} — credentials_enc never exposed
- DELETE /api/cloud/connections/{connection_id}: returns 404 for wrong-owner (prevents enumeration)
- GET /api/cloud/folders/{provider}/{folder_id}: TTL-cached folder listing via get_cloud_folders_cached
- PATCH /api/users/me/default-storage: updates User.default_storage_backend
- _call_cloud_op helper: transparent token refresh + REQUIRES_REAUTH on invalid_grant
- All endpoints use Depends(get_regular_user) — admin gets 403 (D-18, D-19)
2026-05-29 00:40:08 +02:00
curo1305 add654444e docs(05-04): complete WebDAVBackend + NextcloudBackend plan — SUMMARY and STATE
- 05-04-SUMMARY.md: 2 tasks (31 tests, 4 files), 8 min, 1 auto-fixed deviation (factory dispatch)
- STATE.md: plan advanced to 4/8, session log updated, 3 new key decisions recorded
2026-05-28 21:15:12 +02:00
curo1305 6834a6797f docs(05-03): complete GoogleDriveBackend + OneDriveBackend plan
- SUMMARY.md created for Plan 05-03
- STATE.md updated: completed_plans 26→27, progress 81→84%
- Session continuity updated with pytest results (262 passed / 43 xfailed / 1 pre-existing)
- Key decisions added: shared CloudConnectionError, cache_discovery=False, createUploadSession
2026-05-28 21:13:53 +02:00
curo1305 a9ea33dd18 feat(05-04): fix storage factory to dispatch nextcloud to NextcloudBackend
- Previously both 'nextcloud' and 'webdav' providers were dispatched to WebDAVBackend
- Now 'nextcloud' uses NextcloudBackend (has list_folder); 'webdav' uses WebDAVBackend
- Both share identical constructor signature (server_url, username, password)
- Removes type: ignore[import] concern on nextcloud_backend — module now exists
2026-05-28 21:12:27 +02:00
curo1305 1b9573f398 feat(05-04): implement NextcloudBackend extending WebDAVBackend
- NextcloudBackend subclasses WebDAVBackend; inherits all 7 StorageBackend methods
- SSRF guard fully inherited: NextcloudBackend("http://10.0.0.1/dav", ...) raises ValueError
- stores self._username for Nextcloud path convention context
- list_folder(folder_path: str = "") async method added — lists via client.list() +
  client.info() wrapped in asyncio.to_thread(), returns [{id, name, is_dir, size}, ...]
- validate_cloud_url called before every asyncio.to_thread() call in list_folder (D-17)
- health_check overrides parent to use client.check("") for Nextcloud root probe
2026-05-28 21:11:12 +02:00
curo1305 bcb887e61d feat(05-03): implement OneDriveBackend — Microsoft Graph StorageBackend
- CloudConnectionError imported from google_drive_backend (shared exception type)
- CHUNK_SIZE = 10 * 1024 * 1024 (10 MB — above Graph 4 MB limit, Pitfall 6)
- All 7 StorageBackend methods implemented as async coroutines
- Resumable upload sessions (createUploadSession) used for ALL uploads
- _ensure_valid_token() checks expiry with 60s buffer, calls _refresh_token() if expired
- _refresh_token() wraps msal.ConfidentialClientApplication in asyncio.to_thread()
- invalid_grant → CloudConnectionError(reason='invalid_grant') per D-06 / B2 design
- presigned_get_url and generate_presigned_put_url raise NotImplementedError (D-14)
- delete_object silently ignores 404 (no-op per StorageBackend contract)
- Backend is stateless — no DB writes (B2 design)
2026-05-28 21:10:56 +02:00
curo1305 311dfa1513 feat(05-04): implement WebDAVBackend with SSRF guard and asyncio wrapping
- All 7 StorageBackend methods implemented as async coroutines
- validate_cloud_url() called in __init__ (SSRF at construct time) and before
  every asyncio.to_thread() call (D-17 defense-in-depth / T-05-04-01, T-05-04-02)
- _make_path() builds "docuvault/{user_id}/{document_id}{ext}" with urllib.parse.quote
  encoding on path segments (RESEARCH.md Pitfall 2)
- presigned_get_url and generate_presigned_put_url raise NotImplementedError (D-14)
- All webdavclient3 sync calls (upload_to, download_from, clean, info, check, mkdir)
  wrapped in asyncio.to_thread() per MinIOBackend pattern
- delete_object silently ignores missing file exceptions (StorageBackend ABC contract)
2026-05-28 21:09:25 +02:00
curo1305 337ee8ef11 feat(05-03): implement GoogleDriveBackend — Google Drive v3 StorageBackend
- CloudConnectionError(reason=) defined in this module — token_expired | invalid_grant
- All 7 StorageBackend methods implemented as async coroutines
- Every sync googleapiclient call wrapped in asyncio.to_thread() (Pitfall 7)
- cache_discovery=False on build() prevents /tmp directory traversal (T-05-03-05)
- presigned_get_url and generate_presigned_put_url raise NotImplementedError (D-14)
- HttpError 401 raises CloudConnectionError(reason='token_expired')
- HttpError 400 with 'invalid_grant' raises CloudConnectionError(reason='invalid_grant')
- HttpError 404 on delete_object is silently swallowed (no-op per contract)
- Backend is stateless — no DB writes (B2 design, D-05/D-06)
2026-05-28 21:07:26 +02:00
curo1305 c406ab1081 test(05-04): add failing RED tests for WebDAVBackend and NextcloudBackend
- Structure tests: all 7 methods async, proper subclassing
- SSRF guard tests: localhost/127.x/10.x/192.168.x/169.254.x raise ValueError
- NotImplementedError tests for presigned methods
- _make_path path construction and percent-encoding tests
- NextcloudBackend subclass, list_folder, inherited SSRF guard
2026-05-28 21:07:18 +02:00
curo1305 4efe7c1376 test(05-03): add RED phase tests for GoogleDriveBackend and OneDriveBackend
- 32 failing tests covering all 7 StorageBackend methods on both backends
- Verifies CloudConnectionError reason attribute (token_expired / invalid_grant)
- Verifies CHUNK_SIZE == 10 MB (Pitfall 6 prevention)
- Verifies shared CloudConnectionError import across backends
- Verifies _ensure_valid_token skips refresh on non-expired tokens
- Verifies _ensure_valid_token raises CloudConnectionError on invalid_grant
2026-05-28 21:06:14 +02:00
curo1305 3b84626da9 docs(05-02): complete shared cloud utilities plan
- 05-02-SUMMARY.md: full plan summary with TDD gate compliance, deviation docs, threat surface scan
- STATE.md: advanced to plan 26/32 (81%), updated session log, added 4 key decisions
- ROADMAP.md: marked 05-02 complete (2/8 Phase 5 plans done)
2026-05-28 21:04:03 +02:00
curo1305 fb803795fa feat(05-02): implement cloud_cache.py and extend storage factory
- cloud_cache.py: module-level TTLCache(maxsize=1000, ttl=60) singleton with
  threading.Lock for concurrent access safety (RESEARCH.md Pattern 8 / D-16)
- get_cloud_folders_cached(): async function; calls fetch_fn OUTSIDE the lock
  to avoid blocking the event loop during cloud API calls
- invalidate_provider_cache(): removes all cache entries for a user+provider prefix
- storage/__init__.py: adds get_storage_backend_for_document() async factory
  — returns MinIOBackend for minio docs; queries CloudConnection (scoped to user.id),
  decrypts credentials, and lazy-imports cloud backends to avoid circular imports
  — raises HTTPException(503) if connection missing or not ACTIVE (T-05-02-04)
2026-05-28 21:00:48 +02:00
curo1305 976d2ca2de feat(05-02): implement cloud_utils.py — SSRF validation and HKDF credential encryption
- validate_cloud_url(): blocks RFC-1918 (10.x, 172.16.x, 192.168.x), loopback (127.x),
  link-local (169.254.x), IPv6 loopback (::1), ULA (fc00::/7), and 'localhost' string;
  resolves DNS via socket.getaddrinfo BEFORE IP check (anti-DNS-rebinding per D-17)
- _derive_fernet_key(): creates fresh HKDF-SHA256 instance per call (AlreadyFinalized
  pitfall avoided per RESEARCH.md Pitfall 3); uses user_id as salt for per-user isolation
- encrypt_credentials(): Fernet-encrypts JSON-serialised credentials dict; returns str
- decrypt_credentials(): decrypts Fernet token back to original dict
- [Rule 1 - Bug] Fixed test_allows_public_https to use 8.8.8.8 IP (cloud.example.com
  does not resolve in offline CI environments)
2026-05-28 20:58:40 +02:00
curo1305 7fdffddfc1 test(05-02): add failing RED tests for cloud_utils, cloud_cache, and factory
- 11 SSRF validation tests (validate_cloud_url) covering RFC-1918, loopback, link-local, localhost, IPv6
- 7 HKDF credential encryption/decryption round-trip tests (encrypt_credentials, decrypt_credentials)
- 9 TTLCache singleton tests (maxsize=1000, ttl=60, thread-safe lock, get/invalidate helpers)
- 2 storage factory import tests (get_storage_backend_for_document importable)
2026-05-28 20:57:25 +02:00
curo1305 664451b8e6 docs(05-01): complete Wave 0 Nyquist scaffold plan
- Create 05-01-SUMMARY.md documenting all 3 tasks and 5 files modified
- Update STATE.md: session record, progress 78% (25/32 plans), resume file → 05-02
- Update ROADMAP.md: Phase 5 progress (1/8 summaries, In Progress)
- Update REQUIREMENTS.md: mark CLOUD-01..07 complete (Wave 0 scaffold)
2026-05-28 20:54:51 +02:00
curo1305 b53ea863dd feat(05-01): add Phase 5 cloud fixtures to conftest.py
Appends 4 new fixtures to backend/tests/conftest.py:
- mock_google_drive_creds: Google OAuth credential dict (access/refresh token, expiry)
- mock_onedrive_creds: OneDrive MSAL credential dict
- mock_webdav_client: MagicMock with upload_to/download_from/list/check methods
- cloud_connection_factory: async factory that creates CloudConnection ORM rows

All existing fixtures and tests unaffected; pytest collection errors = 0.
2026-05-28 20:51:41 +02:00
curo1305 231dfcd987 test(05-01): create test_cloud.py with 15 Phase 5 xfail stubs
All 15 stubs decorated with @pytest.mark.xfail(strict=False) covering
CLOUD-01..07, D-17 SSRF (test_ssrf_validation parametrized + test_ssrf_link_local),
and SEC-08/IDOR (test_admin_cannot_see_credentials, test_cross_user_idor).
pytest tests/test_cloud.py exits 0 with 19 xfailed (19 = 15 stubs + 4 parametrize variants).
2026-05-28 20:49:18 +02:00
curo1305 a052ed4528 feat(05-01): add Phase 5 cloud storage packages and config settings
- Add 6 new packages to requirements.txt: cryptography>=41.0.0,
  google-auth-oauthlib>=1.3.1, google-api-python-client>=2.196.0,
  msal>=1.36.0, webdavclient3>=3.14.7, cachetools>=5.3.0
- Add 8 new Settings fields to config.py: cloud_creds_key,
  google_client_id/secret, onedrive_client_id/secret/tenant_id,
  backend_url (frontend_url already present from Phase 2)
- Append cloud storage section to .env.example
2026-05-28 20:48:38 +02:00
curo1305 4d7b4c83ab docs(05): create phase 5 plan — cloud storage backends (8 plans, 7 waves)
Plans 05-01..05-08 cover all CLOUD-01..07 requirements plus SEC-09
(cloud credential cleanup on account deletion). Key design decisions:
API layer owns D-05 token refresh + DB update via _call_cloud_op helper;
backends are stateless signal-raisers. Vitest tests added for frontend
store and SettingsCloudTab. RESEARCH.md open questions resolved.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-28 20:25:56 +02:00
curo1305 d13801538d fix(05): revise Phase 5 plans based on checker feedback — B1-B4, W1-W4
B1: Mark RESEARCH.md Open Questions as (RESOLVED) with decision text for all 3
B2: Backends now stateless — raise CloudConnectionError(reason=) only; API layer
    in cloud.py owns token refresh + DB update via _call_cloud_op helper
B3: Add Task 3 to Plan 05 — cloud connection + object cleanup on account deletion (SEC-09)
B4: Add frontend_url setting to Plan 01 Task 1; Plan 05 uses settings.frontend_url
    for OAuth callback redirects
W1: ROADMAP.md Phase 5 now correctly labels Plans 03+04 as Wave 3 (not Wave 2)
W2: Plan 06 invalid_grant test now asserts both 503 HTTP response AND DB REQUIRES_REAUTH
W3: Plan 06 Task 2 split into unit tests (4, cloud_utils.py) and integration tests (11, HTTP)
W4: Plan 07 adds Vitest tests for cloudConnections store (4 tests) and SettingsCloudTab
    mount test (2 tests) per CLAUDE.md testing protocol

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-28 19:55:28 +02:00
curo1305 baa5bed7e2 docs(05): create phase 5 plan — cloud storage backends (8 plans, 7 waves)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-28 19:43:12 +02:00
curo1305 141e582eab docs(05): research phase — cloud storage backends
Verify all 6 PyPI packages (cryptography, google-auth-oauthlib,
google-api-python-client, msal, webdavclient3, cachetools); all pass
slopcheck [OK]. Document HKDF+Fernet pattern, OAuth2 flows for Google
Drive and OneDrive, webdavclient3+asyncio.to_thread for WebDAV/Nextcloud,
SSRF ipaddress module approach, Redis OAuth state pattern, and
cachetools.TTLCache folder listing cache. Confirm cloud_connections table
and storage_backend columns already exist — no new Alembic migration needed.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-28 18:04:11 +02:00
curo1305 358af367f3 docs(05): capture phase 5 context — cloud storage backends
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-28 17:52:25 +02:00
curo1305 d6f742a3c1 chore(phase-4): UAT complete — Phase 4 marked done, sidebar collapse, duplicate-folder fix
UAT: 14/15 passed. Bug fixed: folders/rootFolders array alias in fetchFolders caused
duplicate folder row on creation (rootFolders = [...list] breaks the shared reference).
Sidebar: Folders section now has a collapse/expand chevron, collapsed by default.
State: Phase 4 complete, Phase 5 (Cloud Storage Backends) is next.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-28 17:34:07 +02:00
curo1305 87a32b7ee8 feat(phase-4): complete UX redesign — FileManagerView, FolderTreeItem, test suite, and all Phase 4 fixes
Adds the unified file manager view (Windows Explorer-style), collapsible
folder tree sidebar item, full vitest test suite (55 tests, 4 files), and
commits all Phase 4 backend/frontend fixes that were staged but uncommitted.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-28 17:10:52 +02:00
curo1305 654622d358 wip: phase-4 paused at plan 04-09 — UX redesign + test suite + bug fixes complete
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-28 17:03:32 +02:00
curo1305 81da251669 fix(phase-4): add Move to folder dropdown on DocumentCard hover 2026-05-25 23:17:35 +02:00
curo1305 31f8c00970 fix(phase-4): request() skip res.json() for 204 No Content — fixes delete not updating UI 2026-05-25 22:34:54 +02:00
curo1305 a7c6c9612b fix(phase-4): folders store unwrap .items from list response (push is not a function) 2026-05-25 22:31:41 +02:00
curo1305 b916e485f7 docs(phase-4-09): complete plan 04-09 — Vue UI components and view modifications SUMMARY
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-25 22:17:22 +02:00
curo1305 a3f5fc2e69 feat(phase-4-09): wire components into views — sidebar, cards, home, folder, shared, settings, admin
- AppSidebar: add 'Shared with me' entry (purple icon, count badge) and Folders section with New folder CTA
- DocumentCard: add group class, hover-reveal share button, ShareModal v-if, shared indicator pill
- HomeView: add SearchBar + SortControls above document list; fetchFolders on mount
- FolderView: new view with FolderBreadcrumb, FolderRow list, inline new-subfolder input, document list
- SharedView: new view fetching /api/shares/received with owner_handle display and empty state
- DocumentView: add PDF preview logic (in_app=DocumentPreviewModal, new_tab=window.open); load preferences on mount
- SettingsView: add Document Preferences card with pdf_open_mode radio buttons, auto-save on change
- AdminView: add Audit Log tab alongside Users/Quotas/AI Config tabs
2026-05-25 22:14:12 +02:00
curo1305 36721575a5 feat(phase-4-09): create new components — FolderRow, FolderBreadcrumb, FolderDeleteModal, ShareModal, DocumentPreviewModal, SearchBar, SortControls, AuditLogTab
- FolderRow: inline rename, three-dot menu, delete/rename callbacks, outside-click close
- FolderBreadcrumb: truncation at depth > 4, nav aria-label, ol structure
- FolderDeleteModal: role=dialog, warning icon, doc count in body, Keep/Delete buttons
- ShareModal: handle input, recipients list with revoke, 404/409 error handling
- DocumentPreviewModal: iframe with proxy URL only (never presigned), Escape/overlay close
- SearchBar: role=search, aria-label, Escape clears
- SortControls: aria-pressed, direction indicator, toggle vs switch logic
- AuditLogTab: filters, paginated table, CSV export via window.location.href
- api/client.js: add adminListAuditLog function
2026-05-25 22:10:23 +02:00
curo1305 437c9d134b docs(phase-4): 04-08 execution summary — frontend data layer complete 2026-05-25 22:05:31 +02:00
curo1305 5417f26b93 feat(phase-4): frontend data layer — API client (13 new functions), folders store, documents store extensions, routes
- Extended listDocuments to accept folderId, q, sort, order query params
- Added 6 folder API functions: listFolders, createFolder, getFolder, renameFolder, deleteFolder, moveDocument
- Added 4 share API functions: createShare, listShares, deleteShare, getSharedWithMe
- Added 2 preference API functions: getMyPreferences, updateMyPreferences
- Added getDocumentContentUrl helper (returns URL string, no fetch)
- Created useFoldersStore with full CRUD, navigation state, and breadcrumb support
- Extended useDocumentsStore with currentFolderId, searchQuery, sortField, sortOrder refs
- Added debounced searchQuery watcher (300ms, 2-char minimum, T-04-08-03)
- Added shareDocument, revokeShare, listShares actions to documents store
- Added /folders/:folderId and /shared routes with requiresAuth guard
2026-05-25 21:58:38 +02:00
curo1305 f9141b85b9 docs(phase-4): complete plan 04-07 — SUMMARY.md + STATE.md update 2026-05-25 21:53:31 +02:00
curo1305 8e6005cb73 feat(phase-4): Task 2 — SEC-08 CloudConnectionOut, SEC-09 delete-user cleanup, admin audit writes
- Add CloudConnectionOut Pydantic model (SEC-08): credentials_enc deliberately excluded
- Implement DELETE /api/admin/users/{id} (SEC-09): collects user docs, deletes MinIO
  objects best-effort before DB delete; audit log written within same transaction
- Add write_audit_log calls to: create_user (admin.user_created), update_user_status
  (admin.user_deactivated/admin.user_activated), update_user_quota (admin.quota_changed),
  update_ai_config (admin.ai_provider_assigned), delete_user (admin.user_deleted)
- Add Request param to all admin state-changing handlers for IP extraction
- Fix test_admin_impersonation_not_found: accept 405 in addition to 404/422
  (expected: DELETE /users/{id} exists now, so GET returns 405 — no impersonation
  route still satisfied, just a different HTTP status for non-existent method)
2026-05-25 21:51:34 +02:00
curo1305 e451b16f8f feat(phase-4): Task 1 — audit log backfill in auth.py and documents.py (D-13)
- Add write_audit_log import to auth.py and documents.py
- auth.py: login success (auth.login), login failure (auth.login_failed, no PII),
  logout (auth.logout), logout-all (auth.sign_out_all), change-password
  (auth.password_changed), TOTP enable (auth.totp_enrolled), TOTP disable
  (auth.totp_revoked), backup code used (auth.backup_code_used)
- documents.py: upload confirm (document.uploaded, size+backend only),
  document delete (document.deleted, size only — no filename/extracted_text)
- Add request: Request param to change_password, disable_totp, confirm_upload, delete_document
2026-05-25 21:48:15 +02:00
curo1305 2a0df32e92 feat(phase-4-05): PATCH /api/auth/me/preferences for pdf_open_mode (DOC-01)
- Add PreferencesUpdate Pydantic model with Literal['in_app', 'new_tab'] validation
- Add GET /api/auth/me/preferences — returns current pdf_open_mode
- Add PATCH /api/auth/me/preferences — validates + stores + returns updated value
- Both endpoints use get_current_user (admin can set own prefs, D-10)
- Add 7 preference tests: default GET, in_app, new_tab, invalid 422, persist,
  and two unauthenticated 401 tests
2026-05-25 18:50:52 +02:00
curo1305 f89f787656 feat(phase-4-06): Celery daily audit export task + beat schedule (D-17)
- Create backend/tasks/audit_tasks.py: audit_log_daily_export task
  queries yesterday's AuditLog rows, writes CSV, uploads to MinIO
  audit-logs bucket via put_object_raw(bucket='audit-logs', ...)
- All imports deferred inside _run_daily_export() to prevent circular imports
- celery_app.py: add crontab import, beat entry at midnight UTC,
  tasks.audit_tasks.* routed to documents queue
2026-05-25 18:50:50 +02:00
curo1305 f868a4e0c7 feat(phase-4-05): document streaming proxy GET /api/documents/{id}/content (DOC-02)
- Add _parse_range() helper: validates Range header bounds, raises 416 on invalid
- Add stream_document_content endpoint with get_regular_user dep (admin → 403)
- Access check: owner OR Share.recipient_id; neither → 404
- Bytes fetched via get_object() only — presigned_get_url() never called
- Range requests return 206 + Content-Range header
- Add pdf_open_mode column to User ORM model (migration 0004 already applied)
- Use HTTP_416_RANGE_NOT_SATISFIABLE (non-deprecated constant)
2026-05-25 18:48:32 +02:00
curo1305 364447d0bc feat(phase-4-06): admin audit log viewer + CSV export (ADMIN-06)
- Create backend/api/audit.py: GET /api/admin/audit-log (paginated, filtered)
  and GET /api/admin/audit-log/export (streaming CSV)
- Both endpoints protected by Depends(get_current_admin) — regular users → 403
- _audit_to_dict() whitelist explicitly excludes filename, extracted_text,
  password_hash, credentials_enc (T-04-06-02, D-15)
- CSV export uses same helper as JSON viewer; Content-Disposition: attachment
- Register audit_router in backend/main.py
2026-05-25 18:48:02 +02:00
curo1305 8e6cb6e7d0 test(phase-4-05): add failing tests for document streaming proxy (DOC-02)
- test_content_stream_200: 200 response with correct headers
- test_content_stream_206_range: Range header returns 206 + Content-Range
- test_content_stream_admin_403: admin role blocked by get_regular_user
- test_content_stream_no_presigned_url: presigned_get_url never called
- test_content_stream_share_recipient_200: share recipient access
- test_content_stream_not_found/invalid_id: 404 paths
- test_parse_range_416: out-of-bounds Range header returns 416
2026-05-25 18:47:24 +02:00
curo1305 731857231f docs(phase-4): complete 04-04 Sharing API plan — SUMMARY.md and STATE.md updated
- SUMMARY.md: documents all four endpoints, security invariants, and verification results
- STATE.md: advances to 4/9 plans; adds decisions for IDOR 404, route ordering, quota isolation
2026-05-25 18:45:00 +02:00
curo1305 964128e143 feat(phase-4): Sharing API (SHARE-01..05) — grant by handle, received folder, IDOR-safe revoke
- POST /api/shares: grant share by recipient_handle; 400 self-share, 404 bad UUID/doc/user, 409 duplicate
- GET /api/shares?document_id: list shares owned by current user for a document
- GET /api/shares/received: virtual "shared with me" folder — metadata only (no extracted_text)
- DELETE /api/shares/{share_id}: revoke with IDOR protection (share.owner_id != current_user.id → 404)
- IntegrityError on UniqueConstraint(document_id, recipient_id) → 409
- write_audit_log called for share.granted and share.revoked (D-14)
- /received defined before /{share_id} in router to prevent FastAPI path parameter conflict
- No quota table touched — recipient quota never modified by share operations (T-04-04-04)
2026-05-25 18:43:49 +02:00
curo1305 c6feb5faf2 docs(phase-4): complete 04-03-PLAN.md — Folders API + audit helper
- Create 04-03-SUMMARY.md with full frontmatter, decisions, threat surface scan
- Update STATE.md: plan 3/9, new decisions, session continuity
- Update ROADMAP.md: mark 04-01, 04-02, 04-03 plans complete (3/9)
- Update REQUIREMENTS.md: mark FOLD-01..FOLD-05 complete
2026-05-25 18:40:33 +02:00
curo1305 33a6f9a290 feat(phase-4): Folders API (FOLD-01..05), audit helper (flush-not-commit), document sort/FTS/move
- backend/api/folders.py: POST /api/folders (create), GET /api/folders (list),
  GET /api/folders/{id} (breadcrumb), PATCH /api/folders/{id} (rename),
  DELETE /api/folders/{id} (cascade-delete + atomic quota decrement),
  PATCH /api/documents/{id}/folder (move document)
- All folder endpoints use get_regular_user (admin gets 403); 404 for IDOR
- IntegrityError caught -> 409 on duplicate folder name under same parent
- WITH RECURSIVE CTE for subtree collection with SQLite fallback (OperationalError)
- Atomic quota decrement with CASE WHEN pattern (SQLite compat)
- MinIO object deletion best-effort (per-object try/except)
- write_audit_log called after folder.created, folder.renamed, folder.deleted
- backend/api/documents.py: add sort, order, folder_id, q params to list_documents;
  add is_shared field to each document in response using Share subquery
- backend/main.py: register folders_router and document_move_router
2026-05-25 18:37:22 +02:00
curo1305 259a1542d8 feat(phase-4): add write_audit_log() async helper (flush-not-commit, never-raises)
- Creates backend/services/audit.py with write_audit_log() function
- Uses session.flush() not session.commit() per D-14 architectural requirement
- Catches and logs all exceptions (never re-raises) so audit failure is non-fatal
- Correct AuditLog ORM attribute metadata_ (not metadata) per models.py
2026-05-25 18:33:31 +02:00
curo1305 0ed514907f docs(04-02): complete plan 04-02 summary and state update
- Add 04-02-SUMMARY.md with verification results and decisions
- Update STATE.md to reflect plan 2/9 complete, next action 04-03
2026-05-25 18:31:43 +02:00
curo1305 b6bab5a230 feat(phase-4): Alembic migration 0004 (pdf_open_mode, GIN FTS index, audit-logs bucket) + MinIOBackend.put_object_raw()
- Add users.pdf_open_mode column via batch_alter_table (server_default='in_app')
- Create GIN expression index ix_documents_fts on documents.extracted_text via raw SQL (Alembic #1390)
- Create audit-logs MinIO bucket gated on MINIO_ENDPOINT env var
- Add MinIOBackend.put_object_raw() for caller-supplied bucket+key uploads (audit CSV export)
2026-05-25 18:30:28 +02:00