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 ──────────────────────────────────────────────────────────────────
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"
+1
View File
@@ -6,4 +6,5 @@ dependencies = [
"fastapi>=0.111",
"uvicorn[standard]>=0.29",
"pydantic>=2.0",
"PyJWT>=2.8",
]
+18 -42
View File
@@ -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
+2 -1
View File
@@ -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)}`;