- 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)
- 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
Bugs fixed:
- minio_backend.py: generate_presigned_put_url and presigned_get_url used internal
_client (minio:9000) instead of _public_client (localhost:9000). Browser received
ERR_NAME_NOT_RESOLVED. Fixed by using _public_client with region='us-east-1' to
skip region-discovery HTTP request from inside the container.
- docker-compose.yml: MINIO_API_CORS_ALLOW_ORIGIN was set from CORS_ORIGINS which
uses pydantic JSON list format '["http://localhost:5173"]'. MinIO expected a plain
string and never matched the origin. Fixed to use FRONTEND_URL instead.
- admin.py: All write handlers (create_user, update_user_status, update_user_quota,
update_ai_config) used session.flush() without session.commit(). Changes appeared
to succeed (response reflected in-memory state) but rolled back on session close.
Fixed by replacing flush() with commit() in all four write handlers.
- auth.js: Concurrent refresh() calls from QuotaBar and App.vue on page reload caused
a token rotation race — first call rotated the cookie, second arrived with stale
cookie and cleared accessToken. Fixed by deduplicating with a shared in-flight
promise (_refreshInFlight).
Phase 3 UAT: 9/10 pass. UAT-3 (QuotaBar visual) pending browser confirmation.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Includes planning artifacts (03-CONTEXT, 03-DISCUSSION-LOG, 03-02-SUMMARY),
integration test script, MinIO/auth/docker fixes, and local dev account reference.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- GET /api/admin/users: list users (safe fields only, ordered by created_at)
- POST /api/admin/users: create user (password_must_change=True, quota init)
- PATCH /api/admin/users/{id}/status: deactivate/reactivate with sole-admin guard
- POST /api/admin/users/{id}/password-reset: Celery email dispatch (no token returned)
- GET /api/admin/users/{id}/quota: quota view with MB helpers
- PATCH /api/admin/users/{id}/quota: quota adjust with below-usage warning
- PATCH /api/admin/users/{id}/ai-config: assign AI provider/model per user
- _user_to_dict() whitelist helper prevents password_hash/credentials_enc leakage
- No impersonation endpoint (ADMIN-07 enforced by omission)
- get_current_admin Depends() on every handler (SEC-07)
- Updated backend/main.py to include admin_router
- Fixed test: mock send_reset_email.delay to avoid Redis in unit tests