From 0e5044eb06a05cea74a9f3f9cf2ed7436a886329 Mon Sep 17 00:00:00 2001 From: Davide Scaini Date: Wed, 3 Jun 2026 09:36:20 +0200 Subject: [PATCH] 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. --- bincio/serve/cli.py | 5 +- bincio/serve/deps.py | 1 + bincio/serve/routers/admin.py | 58 +++----- site/src/pages/invites/index.astro | 160 +--------------------- site/src/pages/register/index.astro | 89 +----------- site/src/pages/reset-password/index.astro | 89 +----------- 6 files changed, 37 insertions(+), 365 deletions(-) diff --git a/bincio/serve/cli.py b/bincio/serve/cli.py index ef4a4fb..892a920 100644 --- a/bincio/serve/cli.py +++ b/bincio/serve/cli.py @@ -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") diff --git a/bincio/serve/deps.py b/bincio/serve/deps.py index bb28dfb..c9a1cf2 100644 --- a/bincio/serve/deps.py +++ b/bincio/serve/deps.py @@ -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() diff --git a/bincio/serve/routers/admin.py b/bincio/serve/routers/admin.py index 953e0c8..65c61be 100644 --- a/bincio/serve/routers/admin.py +++ b/bincio/serve/routers/admin.py @@ -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") diff --git a/site/src/pages/invites/index.astro b/site/src/pages/invites/index.astro index 4ebf490..6f9c565 100644 --- a/site/src/pages/invites/index.astro +++ b/site/src/pages/invites/index.astro @@ -1,158 +1,6 @@ --- -import Base from '../../layouts/Base.astro'; +const authUrl = import.meta.env.PUBLIC_AUTH_URL ?? ''; +const target = authUrl ? authUrl + '/invites/' : '/'; --- - -
-
-

Your invites

- -
- - - - - - - -
- - - + + diff --git a/site/src/pages/register/index.astro b/site/src/pages/register/index.astro index d12255d..7bdf4e3 100644 --- a/site/src/pages/register/index.astro +++ b/site/src/pages/register/index.astro @@ -1,87 +1,6 @@ --- -import Base from '../../layouts/Base.astro'; +const authUrl = import.meta.env.PUBLIC_AUTH_URL ?? ''; +const target = authUrl ? authUrl + '/register/' : '/'; --- - -
-

Create account

- -
-
- - -
-
- - -
-
- - -
-
- - -

At least 8 characters

-
- - -
- -

- Already have an account? Sign in -

-
- - - + + diff --git a/site/src/pages/reset-password/index.astro b/site/src/pages/reset-password/index.astro index 1379dad..40ca348 100644 --- a/site/src/pages/reset-password/index.astro +++ b/site/src/pages/reset-password/index.astro @@ -1,87 +1,6 @@ --- -import Base from '../../layouts/Base.astro'; +const authUrl = import.meta.env.PUBLIC_AUTH_URL ?? ''; +const target = authUrl ? authUrl + '/reset-password/' : '/'; --- - -
-

Reset password

-

Enter the reset code you received from the admin.

-

Don't have a code? Contact the instance admin — they can generate one for you from the admin panel. Codes expire after 24 hours.

- -
-
- - -
-
- - -
-
- - -

At least 8 characters

-
- - - -
- -

- Back to sign in -

-
- - - + +