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 ──────────────────────────────────────────────────────────────────
|
# ── 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"
|
||||||
|
|||||||
@@ -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",
|
||||||
]
|
]
|
||||||
|
|||||||
+15
-39
@@ -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,22 +17,20 @@ 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
@@ -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)}`;
|
||||||
|
|||||||
Reference in New Issue
Block a user