- auth.py/auth_routes.py: Login Flow v2 (verified against real NC source/docs) - no OAuth2 client registration needed, backend polls server-side so no CORS issues. Sessions are hashed-token cookies; NC app passwords encrypted at rest (Fernet). Every /api/* route guarded by a blueprint-wide before_request, not per-route decorators, so future routes are protected by default. - receipts/groups scoped per owner_nc_user_id; cross-user access 404s. - nc_client/cospend_client take (username, app_password) per call instead of one shared global credential - each user's uploads/shares/ bills now happen as themselves. - Frontend: LoginGate component drives the login flow (open NC login in a new tab, poll our backend, done). - Merged the old separate review step into the split screen, redesigned with Tailwind (was unstyled/broken), default-excluded-per-item splitting with one-click "include everyone" fixed, DD.MM.YYYY date field, receipt-icon branding. - Dropped LLM bounding-box highlighting - unreliable on real receipts, plain photo upload instead. - Only mention who a bill is split with in its title when there's more than one bill off the same receipt to disambiguate.
82 lines
2.8 KiB
Python
82 lines
2.8 KiB
Python
"""Thin wrapper around the bits of Nextcloud's WebDAV and OCS Share API we need.
|
|
|
|
Auth is HTTP Basic with an app password obtained per-user via Login Flow v2
|
|
(see auth.py) - callers pass (username, app_password) explicitly rather than
|
|
this module reading a single global credential, since every user acts as
|
|
themselves now.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import requests
|
|
|
|
from .config import Config
|
|
|
|
|
|
def _webdav_root(username: str) -> str:
|
|
return f"{Config.NC_BASE_URL}/remote.php/dav/files/{username}"
|
|
|
|
|
|
def ensure_upload_folder(username: str, app_password: str) -> None:
|
|
"""MKCOL each segment of the configured upload folder path if missing.
|
|
|
|
NC_UPLOAD_FOLDER may be nested (e.g. "Documents/Cospend/Assets"); MKCOL
|
|
only creates one level at a time, so walk the path segment by segment.
|
|
Idempotent.
|
|
"""
|
|
if not Config.NC_UPLOAD_FOLDER:
|
|
return
|
|
segments = Config.NC_UPLOAD_FOLDER.split("/")
|
|
partial = ""
|
|
for segment in segments:
|
|
partial = f"{partial}/{segment}" if partial else segment
|
|
url = f"{_webdav_root(username)}/{partial}"
|
|
resp = requests.request("MKCOL", url, auth=(username, app_password), timeout=15)
|
|
# 201 = created, 405 = already exists. Anything else is a real problem.
|
|
if resp.status_code not in (201, 405):
|
|
resp.raise_for_status()
|
|
|
|
|
|
def upload_file(
|
|
username: str, app_password: str, filename: str, content: bytes, content_type: str = "image/jpeg"
|
|
) -> str:
|
|
"""Uploads `content` to <upload folder>/<filename> via WebDAV PUT, into
|
|
the given user's own NC files.
|
|
|
|
Returns the server-relative path (e.g. "wgBill/receipt-123.jpg"), which is
|
|
what the Share API expects as `path`.
|
|
"""
|
|
ensure_upload_folder(username, app_password)
|
|
rel_path = f"{Config.NC_UPLOAD_FOLDER}/{filename}" if Config.NC_UPLOAD_FOLDER else filename
|
|
url = f"{_webdav_root(username)}/{rel_path}"
|
|
resp = requests.put(
|
|
url,
|
|
data=content,
|
|
auth=(username, app_password),
|
|
headers={"Content-Type": content_type},
|
|
timeout=30,
|
|
)
|
|
resp.raise_for_status()
|
|
return rel_path
|
|
|
|
|
|
def create_public_share(username: str, app_password: str, rel_path: str) -> str:
|
|
"""Creates a public read-only link share for `rel_path`. Returns the share URL."""
|
|
url = f"{Config.NC_BASE_URL}/ocs/v2.php/apps/files_sharing/api/v1/shares"
|
|
resp = requests.post(
|
|
url,
|
|
auth=(username, app_password),
|
|
headers={"OCS-APIRequest": "true"},
|
|
data={
|
|
"path": f"/{rel_path}",
|
|
"shareType": 3, # public link
|
|
"permissions": 1, # read-only
|
|
},
|
|
params={"format": "json"},
|
|
timeout=15,
|
|
)
|
|
resp.raise_for_status()
|
|
payload = resp.json()
|
|
share_url = payload["ocs"]["data"]["url"]
|
|
return share_url
|