Files
wgBill/backend/app/auth_routes.py
T
dodox 1b38396a2c 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.
2026-08-30 16:02:29 +02:00

55 lines
1.4 KiB
Python

from __future__ import annotations
from flask import Blueprint, jsonify, request
from . import auth
from .config import Config
bp = Blueprint("auth", __name__)
@bp.post("/login/start")
def login_start():
try:
result = auth.start_login_flow()
except Exception as exc: # noqa: BLE001
return jsonify(error=f"could not start login: {exc}"), 502
return jsonify(result)
@bp.get("/login/poll")
def login_poll():
flow_id = request.args.get("flow_id", "")
if not flow_id:
return jsonify(error="flow_id is required"), 400
try:
result = auth.poll_login_flow(flow_id)
except Exception as exc: # noqa: BLE001
return jsonify(error=f"login poll failed: {exc}"), 502
if result["status"] != "done":
return jsonify(status=result["status"])
resp = jsonify(status="done", nc_user_id=result["nc_user_id"])
resp.set_cookie(
Config.SESSION_COOKIE_NAME,
result["session_id"],
max_age=Config.SESSION_TTL_DAYS * 24 * 3600,
httponly=True,
secure=Config.SESSION_COOKIE_SECURE,
samesite="Lax",
path="/",
)
return resp
@bp.post("/logout")
def logout():
session_id = request.cookies.get(Config.SESSION_COOKIE_NAME)
if session_id:
auth.destroy_session(session_id)
resp = jsonify(status="ok")
resp.delete_cookie(Config.SESSION_COOKIE_NAME, path="/")
return resp