- 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.
102 lines
3.2 KiB
Python
102 lines
3.2 KiB
Python
"""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
|
|
GET /api/{v}/projects/{projectId}/members -> api#getMembers
|
|
POST /api/{v}/projects/{projectId}/bills -> api#createBill
|
|
|
|
lib/Controller/ApiController.php, createBill() signature:
|
|
string $projectId, ?string $date, ?string $what, ?int $payer,
|
|
?string $payedFor, ?float $amount, ... ?string $comment, ...
|
|
-> params are camelCase (payedFor, not payed_for), payer/payedFor are
|
|
numeric member ids (payedFor is a comma-separated string of ids).
|
|
|
|
Being an OCS controller, every call needs the OCS-APIRequest header and the
|
|
full path is under /ocs/v2.php/apps/cospend/...
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
from typing import Any
|
|
|
|
import requests
|
|
|
|
from .config import Config
|
|
|
|
_HEADERS = {"OCS-APIRequest": "true"}
|
|
|
|
|
|
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:
|
|
return root
|
|
return f"{root}/projects/{project_id}"
|
|
|
|
|
|
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=(username, app_password),
|
|
headers=_HEADERS,
|
|
params={"format": "json"},
|
|
timeout=15,
|
|
)
|
|
resp.raise_for_status()
|
|
return resp.json()["ocs"]["data"]
|
|
|
|
|
|
def get_members(username: str, app_password: str, project_id: str) -> list[dict[str, Any]]:
|
|
resp = requests.get(
|
|
f"{_base(project_id)}/members",
|
|
auth=(username, app_password),
|
|
headers=_HEADERS,
|
|
params={"format": "json"},
|
|
timeout=15,
|
|
)
|
|
resp.raise_for_status()
|
|
return resp.json()["ocs"]["data"]
|
|
|
|
|
|
def create_bill(
|
|
username: str,
|
|
app_password: str,
|
|
*,
|
|
project_id: str,
|
|
what: str,
|
|
amount: float,
|
|
payer_id: int,
|
|
ower_ids: list[int],
|
|
date: str,
|
|
comment: str = "",
|
|
) -> int:
|
|
"""Creates a bill split evenly among `ower_ids`, paid by `payer_id`.
|
|
|
|
`amount` is the bill total; Cospend divides it across owers itself.
|
|
Returns the new bill's id - createBill's controller method returns just
|
|
the int id (`return new DataResponse($newBillId)`), not a bill object.
|
|
"""
|
|
resp = requests.post(
|
|
f"{_base(project_id)}/bills",
|
|
auth=(username, app_password),
|
|
headers=_HEADERS,
|
|
params={"format": "json"},
|
|
json={
|
|
"what": what,
|
|
"amount": amount,
|
|
"payer": payer_id,
|
|
"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,
|
|
)
|
|
resp.raise_for_status()
|
|
return resp.json()["ocs"]["data"]
|