feat(core): initialize and use SettingsService; update admin settings API; hook app metadata from settings

This commit is contained in:
Shroominic
2025-09-08 10:52:30 +01:00
parent 14718843de
commit 7761d144f6
3 changed files with 106 additions and 31 deletions
+66 -7
View File
@@ -9,7 +9,6 @@ from pydantic import BaseModel
from sqlmodel import select
from ..wallet import (
TRUSTED_MINTS,
fetch_all_balances,
get_proofs_per_mint_and_unit,
get_wallet,
@@ -18,12 +17,50 @@ from ..wallet import (
)
from .db import ApiKey, create_session
from .logging import get_logger
from .settings import SettingsService, settings
logger = get_logger(__name__)
admin_router = APIRouter(prefix="/admin", include_in_schema=False)
@admin_router.get("/api/settings")
async def get_settings(request: Request) -> dict:
admin_cookie = request.cookies.get("admin_password")
if not admin_cookie or admin_cookie != settings.admin_password:
raise HTTPException(status_code=403, detail="Unauthorized")
data = settings.dict()
if "upstream_api_key" in data:
data["upstream_api_key"] = "[REDACTED]" if data["upstream_api_key"] else ""
if "admin_password" in data:
data["admin_password"] = "[REDACTED]" if data["admin_password"] else ""
if "nsec" in data:
data["nsec"] = "[REDACTED]" if data["nsec"] else ""
return data
class SettingsUpdate(BaseModel):
__root__: dict[str, object]
@admin_router.patch("/api/settings")
async def update_settings(request: Request, update: SettingsUpdate) -> dict:
admin_cookie = request.cookies.get("admin_password")
if not admin_cookie or admin_cookie != settings.admin_password:
raise HTTPException(status_code=403, detail="Unauthorized")
async with create_session() as session:
new_settings = await SettingsService.update(update.__root__, session)
data = new_settings.dict()
if "upstream_api_key" in data:
data["upstream_api_key"] = "[REDACTED]" if data["upstream_api_key"] else ""
if "admin_password" in data:
data["admin_password"] = "[REDACTED]" if data["admin_password"] else ""
if "nsec" in data:
data["nsec"] = "[REDACTED]" if data["nsec"] else ""
return data
class WithdrawRequest(BaseModel):
amount: int
mint_url: str | None = None
@@ -87,7 +124,12 @@ def info(content: str) -> str:
def admin_auth() -> str:
if os.getenv("ADMIN_PASSWORD", "") == "":
try:
settings = SettingsService.get()
admin_pw = settings.admin_password
except Exception:
admin_pw = os.getenv("ADMIN_PASSWORD", "")
if admin_pw == "":
return info("Please set a secure ADMIN_PASSWORD= in your ENV variables.")
else:
return login_form()
@@ -454,7 +496,12 @@ async def dashboard(request: Request) -> str:
@admin_router.get("/", response_class=HTMLResponse)
async def admin(request: Request) -> str:
admin_cookie = request.cookies.get("admin_password")
if admin_cookie and admin_cookie == os.getenv("ADMIN_PASSWORD"):
try:
settings = SettingsService.get()
admin_pw: str = settings.admin_password
except Exception:
admin_pw = os.getenv("ADMIN_PASSWORD", "") or ""
if admin_cookie and admin_cookie == admin_pw:
return await dashboard(request)
return admin_auth()
@@ -462,7 +509,12 @@ async def admin(request: Request) -> str:
@admin_router.get("/logs/{request_id}", response_class=HTMLResponse)
async def view_logs(request: Request, request_id: str) -> str:
admin_cookie = request.cookies.get("admin_password")
if not admin_cookie or admin_cookie != os.getenv("ADMIN_PASSWORD"):
try:
settings = SettingsService.get()
admin_pw: str = settings.admin_password
except Exception:
admin_pw = os.getenv("ADMIN_PASSWORD", "") or ""
if not admin_cookie or admin_cookie != admin_pw:
return admin_auth()
logger.info(f"Investigating logs for request_id: {request_id}")
@@ -660,16 +712,23 @@ async def withdraw(
request: Request, withdraw_request: WithdrawRequest
) -> dict[str, str]:
admin_cookie = request.cookies.get("admin_password")
if not admin_cookie or admin_cookie != os.getenv("ADMIN_PASSWORD"):
try:
settings = SettingsService.get()
admin_pw: str = settings.admin_password
except Exception:
admin_pw = os.getenv("ADMIN_PASSWORD", "") or ""
if not admin_cookie or admin_cookie != admin_pw:
raise HTTPException(status_code=403, detail="Unauthorized")
# Get wallet and check balance
from .settings import settings as global_settings
wallet = await get_wallet(
withdraw_request.mint_url or TRUSTED_MINTS[0], withdraw_request.unit
withdraw_request.mint_url or global_settings.primary_mint, withdraw_request.unit
)
proofs = get_proofs_per_mint_and_unit(
wallet,
withdraw_request.mint_url or TRUSTED_MINTS[0],
withdraw_request.mint_url or global_settings.primary_mint,
withdraw_request.unit,
not_reserved=True,
)
+16 -6
View File
@@ -181,7 +181,12 @@ class SecurityFilter(logging.Filter):
def get_log_level() -> str:
"""Get log level from environment variable."""
level = os.environ.get("LOG_LEVEL", "INFO").upper()
try:
from .settings import settings
level = settings.log_level.upper()
except Exception:
level = os.environ.get("LOG_LEVEL", "INFO").upper()
# Validate log level - if invalid, default to INFO
valid_levels = {"TRACE", "DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL"}
if level not in valid_levels:
@@ -191,11 +196,16 @@ def get_log_level() -> str:
def should_enable_console_logging() -> bool:
"""Check if console logging should be enabled."""
return os.environ.get("ENABLE_CONSOLE_LOGGING", "true").lower() in (
"true",
"1",
"yes",
)
try:
from .settings import settings
return bool(settings.enable_console_logging)
except Exception:
return os.environ.get("ENABLE_CONSOLE_LOGGING", "true").lower() in (
"true",
"1",
"yes",
)
def setup_logging() -> None:
+24 -18
View File
@@ -1,5 +1,4 @@
import asyncio
import os
from contextlib import asynccontextmanager
from typing import AsyncGenerator
@@ -15,10 +14,12 @@ from ..payment.models import MODELS, models_router, update_sats_pricing
from ..proxy import proxy_router
from ..wallet import periodic_payout
from .admin import admin_router
from .db import init_db, run_migrations
from .db import create_session, init_db, run_migrations
from .exceptions import general_exception_handler, http_exception_handler
from .logging import get_logger, setup_logging
from .middleware import LoggingMiddleware
from .settings import SettingsService
from .settings import settings as global_settings
# Initialize logging first
setup_logging()
@@ -47,6 +48,17 @@ async def lifespan(_: FastAPI) -> AsyncGenerator[None, None]:
# This creates any tables that might not be tracked by migrations yet
await init_db()
# Initialize application settings (env -> computed -> DB precedence)
async with create_session() as session:
s = await SettingsService.initialize(session)
# Apply app metadata from settings
try:
app.title = s.name
app.description = s.description
except Exception:
pass
pricing_task = asyncio.create_task(update_sats_pricing())
payout_task = asyncio.create_task(periodic_payout())
nip91_task = asyncio.create_task(announce_provider())
@@ -93,18 +105,12 @@ async def lifespan(_: FastAPI) -> AsyncGenerator[None, None]:
)
app = FastAPI(
version=__version__,
title=os.environ.get("NAME", "ARoutstrNode" + __version__),
description=os.environ.get("DESCRIPTION", "A Routstr Node"),
contact={"name": os.environ.get("NAME", ""), "npub": os.environ.get("NPUB", "")},
lifespan=lifespan,
)
app = FastAPI(version=__version__, lifespan=lifespan)
# Configure CORS
app.add_middleware(
CORSMiddleware,
allow_origins=os.environ.get("CORS_ORIGINS", "*").split(","),
allow_origins=global_settings.cors_origins,
allow_credentials=True,
allow_methods=["*"],
allow_headers=["*"],
@@ -123,14 +129,14 @@ app.add_exception_handler(Exception, general_exception_handler)
@app.get("/v1/info")
async def info() -> dict:
return {
"name": app.title,
"description": app.description,
"name": global_settings.name,
"description": global_settings.description,
"version": __version__,
"npub": os.environ.get("NPUB", ""),
"mints": os.environ.get("CASHU_MINTS", "").split(","),
"http_url": os.environ.get("HTTP_URL", ""),
"onion_url": os.environ.get("ONION_URL", ""),
"models": MODELS,
"npub": global_settings.npub,
"mints": global_settings.cashu_mints,
"http_url": global_settings.http_url,
"onion_url": global_settings.onion_url,
"models": MODELS, # todo maybe remove models from here
}