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/' : '/';
---
-
- Already have an account? Sign in -
-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.
- - - -- Back to sign in -
-