fix: close all bincio-auth migration holes
Pages (register, reset-password, invites) now redirect to bincio.org like login already did. Admin user-state ops (reset-password-code, suspend, unsuspend, delete account) are proxied to bincio-auth via httpx so they write to the correct DB. Adds BINCIO_AUTH_API env var.
This commit is contained in:
+4
-1
@@ -23,11 +23,12 @@ console = Console()
|
||||
@click.option("--dem-url", default=None, envvar="DEM_URL", help="Base URL of an Open-Elevation-compatible API (default: https://api.open-elevation.com).")
|
||||
@click.option("--sync-secret", default=None, envvar="BINCIO_SYNC_SECRET", help="Shared secret for POST /api/internal/rebuild (used by the sync-strava systemd timer).")
|
||||
@click.option("--jwt-secret", default=None, envvar="BINCIO_AUTH_JWT_SECRET", help="Shared JWT secret from bincio-auth. When set, validates JWTs locally instead of DB session lookup.")
|
||||
@click.option("--auth-api", default=None, envvar="BINCIO_AUTH_API", help="Internal URL of the bincio-auth API (e.g. http://127.0.0.1:4040). When set, admin user-state operations are proxied to bincio-auth.")
|
||||
def serve(data_dir: str, site_dir: str | None, host: str, port: int,
|
||||
strava_client_id: str | None, strava_client_secret: str | None,
|
||||
max_users: int | None, public_url: str | None,
|
||||
webroot: str | None, dem_url: str | None,
|
||||
sync_secret: str | None, jwt_secret: str | None) -> None:
|
||||
sync_secret: str | None, jwt_secret: str | None, auth_api: str | None) -> None:
|
||||
"""Start the bincio multi-user application server.
|
||||
|
||||
Handles auth, user management, and write operations.
|
||||
@@ -69,6 +70,8 @@ def serve(data_dir: str, site_dir: str | None, host: str, port: int,
|
||||
deps.sync_secret = sync_secret
|
||||
if jwt_secret:
|
||||
deps.jwt_secret = jwt_secret
|
||||
if auth_api:
|
||||
deps.auth_api = auth_api.rstrip("/")
|
||||
|
||||
db = open_db(dd)
|
||||
current_limit = get_setting(db, "max_users")
|
||||
|
||||
@@ -37,6 +37,7 @@ public_url: str = ""
|
||||
dem_url: str = "https://api.open-elevation.com"
|
||||
sync_secret: str = ""
|
||||
jwt_secret: str = "" # when set, validates JWTs from bincio-auth instead of DB session lookup
|
||||
auth_api: str = "" # when set, proxies user-state admin ops to bincio-auth (e.g. http://127.0.0.1:4040)
|
||||
_db = None
|
||||
_strava_sync_running = False
|
||||
_strava_sync_lock = threading.Lock()
|
||||
|
||||
@@ -9,10 +9,22 @@ import threading
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
import httpx
|
||||
from fastapi import APIRouter, Cookie, HTTPException, Request
|
||||
from fastapi.responses import FileResponse, JSONResponse, StreamingResponse
|
||||
|
||||
from bincio.serve import deps, tasks
|
||||
|
||||
|
||||
async def _auth_proxy(method: str, path: str, cookie: str | None) -> JSONResponse:
|
||||
"""Forward a user-state admin request to bincio-auth and relay the response."""
|
||||
if not deps.auth_api:
|
||||
raise HTTPException(503, "User management is handled by bincio-auth but BINCIO_AUTH_API is not configured.")
|
||||
url = f"{deps.auth_api}{path}"
|
||||
cookies = {"bincio_session": cookie} if cookie else {}
|
||||
async with httpx.AsyncClient() as client:
|
||||
r = await client.request(method, url, cookies=cookies)
|
||||
return JSONResponse(r.json(), status_code=r.status_code)
|
||||
from bincio.serve.models import ResetPasswordCodeResponse
|
||||
from bincio.serve.db import (
|
||||
User,
|
||||
@@ -152,14 +164,8 @@ async def admin_reset_password_code(
|
||||
handle: str,
|
||||
bincio_session: str | None = Cookie(default=None),
|
||||
) -> JSONResponse:
|
||||
"""Generate a one-time password reset code for a user. Admin only."""
|
||||
from bincio.serve.db import create_reset_code
|
||||
admin = deps._require_admin(bincio_session)
|
||||
db = deps._get_db()
|
||||
if not get_user(db, handle):
|
||||
raise HTTPException(404, f"User '{handle}' not found")
|
||||
code = create_reset_code(db, handle, admin.handle)
|
||||
return JSONResponse({"ok": True, "code": code, "expires_in_hours": 24})
|
||||
"""Generate a one-time password reset code for a user. Proxied to bincio-auth."""
|
||||
return await _auth_proxy("POST", f"/api/admin/users/{handle}/reset-password-code", bincio_session)
|
||||
|
||||
|
||||
@router.post("/api/admin/users/{handle}/suspend")
|
||||
@@ -167,18 +173,8 @@ async def admin_suspend(
|
||||
handle: str,
|
||||
bincio_session: str | None = Cookie(default=None),
|
||||
) -> JSONResponse:
|
||||
"""Suspend a user account. Blocks login and invalidates existing sessions. Admin only."""
|
||||
from bincio.serve.db import set_suspended, purge_expired_sessions
|
||||
admin = deps._require_admin(bincio_session)
|
||||
if handle == admin.handle:
|
||||
raise HTTPException(400, "Cannot suspend yourself")
|
||||
db = deps._get_db()
|
||||
if not get_user(db, handle):
|
||||
raise HTTPException(404, "User not found")
|
||||
set_suspended(db, handle, True)
|
||||
db.execute("DELETE FROM sessions WHERE handle = ?", (handle,))
|
||||
db.commit()
|
||||
return JSONResponse({"status": "suspended", "handle": handle})
|
||||
"""Suspend a user account. Proxied to bincio-auth."""
|
||||
return await _auth_proxy("POST", f"/api/admin/users/{handle}/suspend", bincio_session)
|
||||
|
||||
|
||||
@router.post("/api/admin/users/{handle}/unsuspend")
|
||||
@@ -186,14 +182,8 @@ async def admin_unsuspend(
|
||||
handle: str,
|
||||
bincio_session: str | None = Cookie(default=None),
|
||||
) -> JSONResponse:
|
||||
"""Re-enable a suspended user account. Admin only."""
|
||||
from bincio.serve.db import set_suspended
|
||||
deps._require_admin(bincio_session)
|
||||
db = deps._get_db()
|
||||
if not get_user(db, handle):
|
||||
raise HTTPException(404, "User not found")
|
||||
set_suspended(db, handle, False)
|
||||
return JSONResponse({"status": "unsuspended", "handle": handle})
|
||||
"""Re-enable a suspended user account. Proxied to bincio-auth."""
|
||||
return await _auth_proxy("POST", f"/api/admin/users/{handle}/unsuspend", bincio_session)
|
||||
|
||||
|
||||
@router.delete("/api/admin/users/{handle}/account")
|
||||
@@ -201,16 +191,8 @@ async def admin_delete_account(
|
||||
handle: str,
|
||||
bincio_session: str | None = Cookie(default=None),
|
||||
) -> JSONResponse:
|
||||
"""Delete a user account from the database. Data directory is NOT removed. Admin only."""
|
||||
from bincio.serve.db import delete_user as _delete_user
|
||||
admin = deps._require_admin(bincio_session)
|
||||
if handle == admin.handle:
|
||||
raise HTTPException(400, "Cannot delete your own account")
|
||||
db = deps._get_db()
|
||||
if not get_user(db, handle):
|
||||
raise HTTPException(404, "User not found")
|
||||
_delete_user(db, handle)
|
||||
return JSONResponse({"status": "deleted", "handle": handle})
|
||||
"""Delete a user account. Proxied to bincio-auth."""
|
||||
return await _auth_proxy("DELETE", f"/api/admin/users/{handle}/account", bincio_session)
|
||||
|
||||
|
||||
@router.post("/api/admin/users/{handle}/rebuild")
|
||||
|
||||
Reference in New Issue
Block a user