Switch auth to bincio-auth JWT; fix login redirect to bincio.org
This commit is contained in:
@@ -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"
|
||||
|
||||
@@ -6,4 +6,5 @@ dependencies = [
|
||||
"fastapi>=0.111",
|
||||
"uvicorn[standard]>=0.29",
|
||||
"pydantic>=2.0",
|
||||
"PyJWT>=2.8",
|
||||
]
|
||||
|
||||
+18
-42
@@ -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
@@ -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)}`;
|
||||
|
||||
Reference in New Issue
Block a user