diff --git a/.gitignore b/.gitignore index 3333d1c9..f9db7ffb 100644 --- a/.gitignore +++ b/.gitignore @@ -38,8 +38,3 @@ proof_backups *.todo ui_out -output/ -.pnpm-store/ - -# env files -.env* diff --git a/pyproject.toml b/pyproject.toml index c3dc2f16..71f69194 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "routstr" -version = "0.4.0" +version = "0.3.0" description = "Payment proxy for your LLM endpoint using cashu and nostr." readme = "README.md" requires-python = ">=3.11" diff --git a/routstr/core/admin.py b/routstr/core/admin.py index cdfb2e86..7f429e18 100644 --- a/routstr/core/admin.py +++ b/routstr/core/admin.py @@ -2,7 +2,6 @@ import json import secrets from datetime import datetime, timezone from pathlib import Path -from typing import NoReturn from fastapi import APIRouter, Depends, HTTPException, Query, Request from pydantic import BaseModel @@ -27,55 +26,20 @@ logger = get_logger(__name__) admin_router = APIRouter(prefix="/admin", include_in_schema=False) admin_sessions: dict[str, int] = {} -ADMIN_SESSION_DURATION = 12 * 60 * 60 +ADMIN_SESSION_DURATION = 3600 # Usage analytics remain queryable up to 12 months. MAX_USAGE_ANALYTICS_HOURS = 365 * 24 -def _current_timestamp() -> int: - return int(datetime.now(timezone.utc).timestamp()) - - -def _cleanup_expired_admin_sessions(now_timestamp: int | None = None) -> None: - current_timestamp = ( - now_timestamp if now_timestamp is not None else _current_timestamp() - ) - expired_tokens = [ - token - for token, expiry_timestamp in admin_sessions.items() - if expiry_timestamp <= current_timestamp - ] - for token in expired_tokens: - admin_sessions.pop(token, None) - - -def _raise_unauthorized(detail: str) -> NoReturn: - raise HTTPException( - status_code=401, - detail=detail, - headers={"WWW-Authenticate": "Bearer"}, - ) - - def require_admin_api(request: Request) -> None: - auth_header = request.headers.get("Authorization", "") - if not auth_header.startswith("Bearer "): - _raise_unauthorized("Missing bearer token") + auth_header = request.headers.get("Authorization") + if auth_header and auth_header.startswith("Bearer "): + token = auth_header.split(" ", 1)[1] + expiry = admin_sessions.get(token) + if expiry and expiry > int(datetime.now(timezone.utc).timestamp()): + return - token = auth_header.split(" ", 1)[1].strip() - if not token: - _raise_unauthorized("Missing bearer token") - - now_timestamp = _current_timestamp() - expiry_timestamp = admin_sessions.get(token) - if expiry_timestamp is None: - _raise_unauthorized("Invalid session token") - - if expiry_timestamp <= now_timestamp: - admin_sessions.pop(token, None) - _raise_unauthorized("Session expired") - - _cleanup_expired_admin_sessions(now_timestamp) + raise HTTPException(status_code=403, detail="Unauthorized") @admin_router.get("/api/temporary-balances", dependencies=[Depends(require_admin_api)]) @@ -244,10 +208,18 @@ async def admin_login( raise HTTPException(status_code=401, detail="Invalid password") token = secrets.token_urlsafe(32) - expiry_timestamp = _current_timestamp() + ADMIN_SESSION_DURATION + expiry_timestamp = ( + int(datetime.now(timezone.utc).timestamp()) + ADMIN_SESSION_DURATION + ) admin_sessions[token] = expiry_timestamp - _cleanup_expired_admin_sessions() + expired_tokens = [ + t + for t, exp in admin_sessions.items() + if exp <= int(datetime.now(timezone.utc).timestamp()) + ] + for t in expired_tokens: + del admin_sessions[t] return {"ok": True, "token": token, "expires_in": ADMIN_SESSION_DURATION} diff --git a/routstr/core/main.py b/routstr/core/main.py index 4f8da4f4..3785323f 100644 --- a/routstr/core/main.py +++ b/routstr/core/main.py @@ -31,9 +31,9 @@ setup_logging() logger = get_logger(__name__) if os.getenv("VERSION_SUFFIX") is not None: - __version__ = f"0.4.0-{os.getenv('VERSION_SUFFIX')}" + __version__ = f"0.3.0-{os.getenv('VERSION_SUFFIX')}" else: - __version__ = "0.4.0" + __version__ = "0.3.0" @asynccontextmanager diff --git a/tests/integration/test_general_info_endpoints.py b/tests/integration/test_general_info_endpoints.py index 48528793..d9399f09 100644 --- a/tests/integration/test_general_info_endpoints.py +++ b/tests/integration/test_general_info_endpoints.py @@ -264,13 +264,12 @@ async def test_models_endpoint_accept_headers(integration_client: AsyncClient) - async def test_admin_endpoint_unauthenticated( integration_client: AsyncClient, db_snapshot: Any ) -> None: - """Test unauthenticated access to admin settings endpoint is rejected.""" + """Test GET /admin/ endpoint redirects to /""" await db_snapshot.capture() response = await integration_client.get("/admin/api/settings") - assert response.status_code == 401 - assert response.headers.get("www-authenticate") == "Bearer" + assert response.status_code == 403 diff = await db_snapshot.diff() assert len(diff["api_keys"]["added"]) == 0 diff --git a/uv.lock b/uv.lock index 388e2567..facd5d22 100644 --- a/uv.lock +++ b/uv.lock @@ -1878,7 +1878,7 @@ wheels = [ [[package]] name = "routstr" -version = "0.4.0" +version = "0.3.0" source = { editable = "." } dependencies = [ { name = "aiosqlite" },