3.9 KiB
CRM Step 09 — Integration: Nextcloud WebDAV
Context
Read .claude/crm-build-plan.md for full context and IMPORTANT NOTES.
Steps 01–08 must be complete.
Task
Connect the console to Nextcloud via WebDAV so that:
- Files in a customer's Nextcloud folder are listed in the Media tab automatically
- Uploading a file from the console sends it to Nextcloud
- Files can be downloaded/previewed via a backend proxy
Backend changes
1. Add Nextcloud settings to backend/config.py
nextcloud_url: str = "https://nextcloud.bonamin.gr" # e.g. https://cloud.example.com
nextcloud_email: str = "bellsystems.gr@gmail.com"
nextcloud_username: str = "bellsystems-console"
nextcloud_password: str = "ydE916VdaQdbP2CQGhD!"
nextcloud_app_password: str = "rtLCp-NCy3y-gNZdg-38MtN-r8D2N"
nextcloud_base_path: str = "BellSystems" # root folder inside Nextcloud
2. Create backend/crm/nextcloud.py
WebDAV client using httpx (already available). Functions:
async def list_folder(nextcloud_path: str) -> List[dict]:
"""
PROPFIND request to Nextcloud WebDAV.
Returns list of {filename, path, mime_type, size, last_modified, is_dir}
Parse the XML response (use xml.etree.ElementTree).
URL: {nextcloud_url}/remote.php/dav/files/{username}/{nextcloud_base_path}/{nextcloud_path}
"""
async def upload_file(nextcloud_path: str, filename: str, content: bytes, mime_type: str) -> str:
"""
PUT request to upload file.
Returns the full nextcloud_path of the uploaded file.
"""
async def download_file(nextcloud_path: str) -> tuple[bytes, str]:
"""
GET request. Returns (content_bytes, mime_type).
"""
async def delete_file(nextcloud_path: str) -> None:
"""
DELETE request.
"""
Use HTTP Basic Auth with nextcloud_username/nextcloud_password. If nextcloud_url is empty string, raise HTTPException 503 "Nextcloud not configured".
3. Add to backend/crm/router.py
Media/Nextcloud endpoints:
GET /api/crm/nextcloud/browse?path=05_Customers/FOLDER
→ calls list_folder(path), returns file list
GET /api/crm/nextcloud/file?path=05_Customers/FOLDER/photo.jpg
→ calls download_file(path), returns Response(content=bytes, media_type=mime_type)
→ This is the proxy endpoint — frontend uses this to display images
POST /api/crm/nextcloud/upload
→ accepts UploadFile + form field nextcloud_path (destination folder)
→ calls upload_file(...), then calls create_media(...) to save the metadata record
→ returns the created MediaInDB
DELETE /api/crm/nextcloud/file?path=...
→ calls delete_file(path), also deletes the matching crm_media record if found
Frontend changes
Update Media tab in CustomerDetail.jsx
- On load: if
customer.nextcloud_folderis set, fetchGET /api/crm/nextcloud/browse?path={customer.nextcloud_folder}and merge results with existingcrm_mediarecords. Show files from both sources — deduplicate by nextcloud_path. - Image files: render as
<img src="/api/crm/nextcloud/file?path=..." />via the proxy endpoint - Other files: show as a download link hitting the same proxy endpoint
- Upload button: file picker → POST to
/api/crm/nextcloud/uploadwith file + destination path (default to customer's Sent Media subfolder) - Show upload progress indicator
Update Media tab in CustomerDetail.jsx — subfolder selector
When uploading, let user choose subfolder: "Sent Media" | "Received Media" | "Internal" (maps to direction field too)
Notes
httpxis likely already in requirements. If not, add it:httpx>=0.27.0- PROPFIND response is XML (DAV namespace). Parse
D:responseelements, extractD:hrefandD:propchildren. - The proxy approach means the VPS never stores files — it just streams them through from Nextcloud
- nextcloud_base_path in config allows the root to be
BellSystemsso paths in DB are relative to that root