Per-user auth via Nextcloud Login Flow v2, Tailwind UI rewrite

- 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.
This commit is contained in:
2026-08-30 16:02:29 +02:00
parent d97aac0e17
commit 1b38396a2c
31 changed files with 1653 additions and 746 deletions
+14 -12
View File
@@ -1,6 +1,7 @@
"""Cospend's authenticated (NC-login) API - same app-password auth as
nc_client. This is an OCS API (same family as the Share API), confirmed
against the real routes/controller source in julien-nc/cospend-nc:
"""Cospend's authenticated (NC-login) API - same per-user app-password auth
as nc_client (see auth.py). This is an OCS API (same family as the Share
API), confirmed against the real routes/controller source in
julien-nc/cospend-nc:
appinfo/routes.php ('ocs' section):
GET /api/{v}/projects -> api#getLocalProjects
@@ -28,10 +29,6 @@ from .config import Config
_HEADERS = {"OCS-APIRequest": "true"}
def _auth() -> tuple[str, str]:
return (Config.NC_USERNAME, Config.NC_APP_PASSWORD)
def _base(project_id: str | None = None) -> str:
root = f"{Config.NC_BASE_URL}/ocs/v2.php/apps/cospend/api/v1"
if project_id is None:
@@ -39,11 +36,11 @@ def _base(project_id: str | None = None) -> str:
return f"{root}/projects/{project_id}"
def get_projects() -> list[dict[str, Any]]:
def get_projects(username: str, app_password: str) -> list[dict[str, Any]]:
"""Lists Cospend projects visible to the authenticated user."""
resp = requests.get(
f"{_base()}/projects",
auth=_auth(),
auth=(username, app_password),
headers=_HEADERS,
params={"format": "json"},
timeout=15,
@@ -52,10 +49,10 @@ def get_projects() -> list[dict[str, Any]]:
return resp.json()["ocs"]["data"]
def get_members(project_id: str) -> list[dict[str, Any]]:
def get_members(username: str, app_password: str, project_id: str) -> list[dict[str, Any]]:
resp = requests.get(
f"{_base(project_id)}/members",
auth=_auth(),
auth=(username, app_password),
headers=_HEADERS,
params={"format": "json"},
timeout=15,
@@ -65,6 +62,8 @@ def get_members(project_id: str) -> list[dict[str, Any]]:
def create_bill(
username: str,
app_password: str,
*,
project_id: str,
what: str,
@@ -82,7 +81,7 @@ def create_bill(
"""
resp = requests.post(
f"{_base(project_id)}/bills",
auth=_auth(),
auth=(username, app_password),
headers=_HEADERS,
params={"format": "json"},
json={
@@ -92,6 +91,9 @@ def create_bill(
"payedFor": ",".join(str(i) for i in ower_ids),
"comment": comment,
"date": date, # YYYY-MM-DD, the receipt's issue date, not today
# required by LocalProjectService::createBill (400s without it);
# 'n' = FREQUENCY_NO, i.e. this bill doesn't repeat.
"repeat": "n",
},
timeout=15,
)