Switch auth to bincio-auth JWT; fix login redirect to bincio.org

This commit is contained in:
Davide Scaini
2026-06-03 21:00:29 +02:00
parent 15a993ecc6
commit 3d09097eb3
4 changed files with 23 additions and 44 deletions
+2 -1
View File
@@ -10,7 +10,7 @@ VPS=root@95.216.55.151
# ── Frontend ────────────────────────────────────────────────────────────────── # ── Frontend ──────────────────────────────────────────────────────────────────
echo "Building 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…" echo "Deploying frontend…"
rsync -az --delete dist/ "$VPS:/var/www/planner/" rsync -az --delete dist/ "$VPS:/var/www/planner/"
@@ -23,6 +23,7 @@ if [[ -z "$SERVER_DIR" ]]; then
exit 1 exit 1
fi fi
rsync -az server/server.py server/pyproject.toml "$VPS:$SERVER_DIR/" rsync -az server/server.py server/pyproject.toml "$VPS:$SERVER_DIR/"
ssh "$VPS" "cd $SERVER_DIR && uv sync -q"
echo "Restarting bincio-planner.service…" echo "Restarting bincio-planner.service…"
ssh "$VPS" "systemctl restart bincio-planner.service && systemctl is-active bincio-planner.service" ssh "$VPS" "systemctl restart bincio-planner.service && systemctl is-active bincio-planner.service"
+1
View File
@@ -6,4 +6,5 @@ dependencies = [
"fastapi>=0.111", "fastapi>=0.111",
"uvicorn[standard]>=0.29", "uvicorn[standard]>=0.29",
"pydantic>=2.0", "pydantic>=2.0",
"PyJWT>=2.8",
] ]
+18 -42
View File
@@ -1,14 +1,15 @@
"""BincioPlanner API — save/load route plans. """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 Storage: JSON files at $PLANS_DIR/{handle}/{plan_id}.json
Collections: $PLANS_DIR/{handle}/_collections.json Collections: $PLANS_DIR/{handle}/_collections.json
Shared plans: $PLANS_DIR/_shared/{plan_id}.json (any authed user can read/write/delete) 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 Run: uvicorn server:app --host 127.0.0.1 --port 8002
Env vars: Env vars:
SHARED_DB_PATH path to bincio_activity instance.db (default: /var/bincio/data/instance.db) 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) PLANS_DIR root dir for plan JSON files (default: /var/bincio_planner)
DEV_HANDLE bypass auth in local dev
""" """
from __future__ import annotations from __future__ import annotations
@@ -16,23 +17,21 @@ import json
import os import os
import re import re
import secrets import secrets
import sqlite3
import time import time
from contextlib import contextmanager
from dataclasses import dataclass from dataclasses import dataclass
from pathlib import Path from pathlib import Path
from typing import Optional from typing import Optional
import jwt as _jwt
from fastapi import Cookie, Depends, FastAPI, HTTPException, Query from fastapi import Cookie, Depends, FastAPI, HTTPException, Query
from fastapi.middleware.cors import CORSMiddleware from fastapi.middleware.cors import CORSMiddleware
from fastapi.middleware.gzip import GZipMiddleware from fastapi.middleware.gzip import GZipMiddleware
from fastapi.responses import JSONResponse from fastapi.responses import JSONResponse
from pydantic import BaseModel from pydantic import BaseModel
_SHARED_DB_PATH = Path(os.environ.get("SHARED_DB_PATH", "/var/bincio/data/instance.db")) _JWT_SECRET = os.environ.get("BINCIO_AUTH_JWT_SECRET", "")
_PLANS_DIR = Path(os.environ.get("PLANS_DIR", "/var/bincio_planner")) _PLANS_DIR = Path(os.environ.get("PLANS_DIR", "/var/bincio_planner"))
_SESSION_COOKIE = "bincio_session" _DEV_HANDLE = os.environ.get("DEV_HANDLE", "")
_DEV_HANDLE = os.environ.get("DEV_HANDLE", "")
_SAFE_ID = re.compile(r"^[A-Za-z0-9_-]{1,32}$") _SAFE_ID = re.compile(r"^[A-Za-z0-9_-]{1,32}$")
@@ -45,42 +44,19 @@ class User:
display_name: str display_name: str
@contextmanager def _decode_session(token: str) -> Optional[User]:
def _db(): if not _JWT_SECRET:
if not _SHARED_DB_PATH.exists(): return None
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")
try: try:
yield con payload = _jwt.decode(token, _JWT_SECRET, algorithms=["HS256"])
finally: except _jwt.PyJWTError:
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:
return None return None
if not row: handle = payload.get("sub")
if not handle:
return None return None
if row["expires_at"] < int(time.time()): if not payload.get("activity_access"):
return None return None
if not row["activity_access"]: return User(handle=handle, display_name=payload.get("display_name", handle))
return None
if row["suspended"]:
return None
return User(handle=row["handle"], display_name=row["display_name"])
async def require_auth(bincio_session: Optional[str] = Cookie(default=None)) -> User: 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) return User(handle=_DEV_HANDLE, display_name=_DEV_HANDLE)
if not bincio_session: if not bincio_session:
raise HTTPException(401, "Authentication required") raise HTTPException(401, "Authentication required")
user = _get_session_user(bincio_session) user = _decode_session(bincio_session)
if not user: if not user:
raise HTTPException(401, "Authentication required") raise HTTPException(401, "Authentication required")
return user return user
+2 -1
View File
@@ -3,6 +3,7 @@
import Planner from './Planner.svelte'; import Planner from './Planner.svelte';
const ACTIVITY_URL = import.meta.env.VITE_ACTIVITY_URL ?? 'https://activity.bincio.org'; 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 = { const PALETTES = {
default: { accent: '#60a5fa', dim: 'rgba(96,165,250,0.15)' }, default: { accent: '#60a5fa', dim: 'rgba(96,165,250,0.15)' },
@@ -39,7 +40,7 @@
authed = true; authed = true;
activityAccess = me.activity_access ?? false; activityAccess = me.activity_access ?? false;
} else { } else {
window.location.href = `${ACTIVITY_URL}/login/?next=${encodeURIComponent(window.location.href)}`; window.location.href = `${AUTH_URL}/login/?next=${encodeURIComponent(window.location.href)}`;
} }
} catch { } catch {
window.location.href = `${ACTIVITY_URL}/login/?next=${encodeURIComponent(window.location.href)}`; window.location.href = `${ACTIVITY_URL}/login/?next=${encodeURIComponent(window.location.href)}`;