diff --git a/deploy.sh b/deploy.sh index 6457b65..9808042 100755 --- a/deploy.sh +++ b/deploy.sh @@ -10,7 +10,7 @@ VPS=root@95.216.55.151 # ── Frontend ────────────────────────────────────────────────────────────────── echo "Building frontend…" -VITE_ACTIVITY_URL=https://activity.bincio.org VITE_PLANNER_API_URL= npm run build +VITE_ACTIVITY_URL=https://activity.bincio.org VITE_AUTH_URL=https://bincio.org VITE_PLANNER_API_URL= npm run build echo "Deploying frontend…" rsync -az --delete dist/ "$VPS:/var/www/planner/" @@ -23,6 +23,7 @@ if [[ -z "$SERVER_DIR" ]]; then exit 1 fi rsync -az server/server.py server/pyproject.toml "$VPS:$SERVER_DIR/" +ssh "$VPS" "cd $SERVER_DIR && uv sync -q" echo "Restarting bincio-planner.service…" ssh "$VPS" "systemctl restart bincio-planner.service && systemctl is-active bincio-planner.service" diff --git a/server/pyproject.toml b/server/pyproject.toml index 9706011..49648d6 100644 --- a/server/pyproject.toml +++ b/server/pyproject.toml @@ -6,4 +6,5 @@ dependencies = [ "fastapi>=0.111", "uvicorn[standard]>=0.29", "pydantic>=2.0", + "PyJWT>=2.8", ] diff --git a/server/server.py b/server/server.py index c0faa79..815a00a 100644 --- a/server/server.py +++ b/server/server.py @@ -1,14 +1,15 @@ """BincioPlanner API — save/load route plans. -Auth: reads bincio_session cookie, validates against shared instance.db (same strategy as bincio_wiki). +Auth: validates bincio_session JWT issued by bincio-auth (HS256, shared secret). Storage: JSON files at $PLANS_DIR/{handle}/{plan_id}.json Collections: $PLANS_DIR/{handle}/_collections.json Shared plans: $PLANS_DIR/_shared/{plan_id}.json (any authed user can read/write/delete) Run: uvicorn server:app --host 127.0.0.1 --port 8002 Env vars: - SHARED_DB_PATH path to bincio_activity instance.db (default: /var/bincio/data/instance.db) - PLANS_DIR root dir for plan JSON files (default: /var/bincio_planner) + BINCIO_AUTH_JWT_SECRET HS256 secret shared with bincio-auth (required in production) + PLANS_DIR root dir for plan JSON files (default: /var/bincio_planner) + DEV_HANDLE bypass auth in local dev """ from __future__ import annotations @@ -16,23 +17,21 @@ import json import os import re import secrets -import sqlite3 import time -from contextlib import contextmanager from dataclasses import dataclass from pathlib import Path from typing import Optional +import jwt as _jwt from fastapi import Cookie, Depends, FastAPI, HTTPException, Query from fastapi.middleware.cors import CORSMiddleware from fastapi.middleware.gzip import GZipMiddleware from fastapi.responses import JSONResponse from pydantic import BaseModel -_SHARED_DB_PATH = Path(os.environ.get("SHARED_DB_PATH", "/var/bincio/data/instance.db")) -_PLANS_DIR = Path(os.environ.get("PLANS_DIR", "/var/bincio_planner")) -_SESSION_COOKIE = "bincio_session" -_DEV_HANDLE = os.environ.get("DEV_HANDLE", "") +_JWT_SECRET = os.environ.get("BINCIO_AUTH_JWT_SECRET", "") +_PLANS_DIR = Path(os.environ.get("PLANS_DIR", "/var/bincio_planner")) +_DEV_HANDLE = os.environ.get("DEV_HANDLE", "") _SAFE_ID = re.compile(r"^[A-Za-z0-9_-]{1,32}$") @@ -45,42 +44,19 @@ class User: display_name: str -@contextmanager -def _db(): - if not _SHARED_DB_PATH.exists(): - raise HTTPException(503, f"Shared DB not found at {_SHARED_DB_PATH}. " - "Set SHARED_DB_PATH or run bincio_activity first.") - con = sqlite3.connect(_SHARED_DB_PATH, check_same_thread=False) - con.row_factory = sqlite3.Row - con.execute("PRAGMA journal_mode=WAL") +def _decode_session(token: str) -> Optional[User]: + if not _JWT_SECRET: + return None try: - yield con - finally: - con.close() - - -def _get_session_user(token: str) -> Optional[User]: - try: - with _db() as con: - row = con.execute( - "SELECT s.handle, s.expires_at, u.display_name, u.activity_access, u.suspended " - "FROM sessions s JOIN users u ON s.handle = u.handle " - "WHERE s.token = ?", - (token,), - ).fetchone() - except HTTPException: - raise - except Exception: + payload = _jwt.decode(token, _JWT_SECRET, algorithms=["HS256"]) + except _jwt.PyJWTError: return None - if not row: + handle = payload.get("sub") + if not handle: return None - if row["expires_at"] < int(time.time()): + if not payload.get("activity_access"): return None - if not row["activity_access"]: - return None - if row["suspended"]: - return None - return User(handle=row["handle"], display_name=row["display_name"]) + return User(handle=handle, display_name=payload.get("display_name", handle)) async def require_auth(bincio_session: Optional[str] = Cookie(default=None)) -> User: @@ -88,7 +64,7 @@ async def require_auth(bincio_session: Optional[str] = Cookie(default=None)) -> return User(handle=_DEV_HANDLE, display_name=_DEV_HANDLE) if not bincio_session: raise HTTPException(401, "Authentication required") - user = _get_session_user(bincio_session) + user = _decode_session(bincio_session) if not user: raise HTTPException(401, "Authentication required") return user diff --git a/src/App.svelte b/src/App.svelte index 2f24823..1f93824 100644 --- a/src/App.svelte +++ b/src/App.svelte @@ -3,6 +3,7 @@ import Planner from './Planner.svelte'; const ACTIVITY_URL = import.meta.env.VITE_ACTIVITY_URL ?? 'https://activity.bincio.org'; + const AUTH_URL = import.meta.env.VITE_AUTH_URL ?? 'https://bincio.org'; const PALETTES = { default: { accent: '#60a5fa', dim: 'rgba(96,165,250,0.15)' }, @@ -39,7 +40,7 @@ authed = true; activityAccess = me.activity_access ?? false; } else { - window.location.href = `${ACTIVITY_URL}/login/?next=${encodeURIComponent(window.location.href)}`; + window.location.href = `${AUTH_URL}/login/?next=${encodeURIComponent(window.location.href)}`; } } catch { window.location.href = `${ACTIVITY_URL}/login/?next=${encodeURIComponent(window.location.href)}`;