fix
This commit is contained in:
126
app/api/admin.py
126
app/api/admin.py
@@ -40,6 +40,76 @@ _logger = get_logger(__name__)
|
||||
router = APIRouter(prefix="/api/admin", tags=["admin"])
|
||||
|
||||
|
||||
# --------------------------------------------------------------------------- #
|
||||
# Admin recovery — create a new admin when all existing admins lost access.
|
||||
# This endpoint is NOT behind require_admin (it's for recovery). It requires
|
||||
# the admin.setup_token from settings, which is printed on every startup.
|
||||
# --------------------------------------------------------------------------- #
|
||||
@router.post("/recover", response_model=dict)
|
||||
async def recover_admin(
|
||||
body: dict,
|
||||
db: AsyncSession = Depends(get_db),
|
||||
) -> dict:
|
||||
"""Create a new admin user using the admin setup token.
|
||||
|
||||
This endpoint is for disaster recovery when all existing admins have lost
|
||||
access. It requires the `admin.setup_token` (printed on backend startup)
|
||||
and creates a new admin user.
|
||||
|
||||
Body: {token, email, username, password}
|
||||
"""
|
||||
from app.core.security import hash_password, validate_password_strength
|
||||
from app.core.settings_service import get_admin_setup_token
|
||||
from app.models import User as UserModel
|
||||
from sqlalchemy import or_
|
||||
|
||||
token = body.get("token", "")
|
||||
expected_token = await get_admin_setup_token(db)
|
||||
if token != expected_token:
|
||||
raise HTTPException(403, "invalid_admin_token")
|
||||
|
||||
email = body.get("email", "").strip()
|
||||
username = body.get("username", "").strip()
|
||||
password = body.get("password", "")
|
||||
|
||||
if not email or not username or not password:
|
||||
raise HTTPException(400, "email, username, and password are required")
|
||||
|
||||
errors = validate_password_strength(password)
|
||||
if errors:
|
||||
raise HTTPException(400, errors[0])
|
||||
|
||||
existing = (
|
||||
await db.execute(
|
||||
select(UserModel).where(
|
||||
or_(UserModel.email == email, UserModel.username == username)
|
||||
)
|
||||
)
|
||||
).scalar_one_or_none()
|
||||
if existing is not None:
|
||||
if existing.email == email:
|
||||
raise HTTPException(400, "email_already_exists")
|
||||
raise HTTPException(400, "username_already_exists")
|
||||
|
||||
user = UserModel(
|
||||
email=email,
|
||||
username=username,
|
||||
password_hash=hash_password(password),
|
||||
is_admin=True,
|
||||
is_active=True,
|
||||
)
|
||||
db.add(user)
|
||||
await db.commit()
|
||||
await db.refresh(user)
|
||||
return {
|
||||
"ok": True,
|
||||
"id": str(user.id),
|
||||
"email": user.email,
|
||||
"username": user.username,
|
||||
"is_admin": user.is_admin,
|
||||
}
|
||||
|
||||
|
||||
# --------------------------------------------------------------------------- #
|
||||
# Settings
|
||||
# --------------------------------------------------------------------------- #
|
||||
@@ -95,8 +165,13 @@ async def list_llm_logs(
|
||||
total = (await db.execute(select(func.count()).select_from(stmt.subquery()))).scalar_one()
|
||||
stmt = stmt.order_by(LlmCallLog.created_at.desc()).offset((page - 1) * per_page).limit(per_page)
|
||||
rows = (await db.execute(stmt)).scalars().all()
|
||||
items = []
|
||||
for r in rows:
|
||||
d = LlmLogOut.model_validate(r).model_dump(mode="json")
|
||||
d["world_id"] = str(r.world_id) if r.world_id else None
|
||||
items.append(d)
|
||||
return {
|
||||
"items": [LlmLogOut.model_validate(r).model_dump(mode="json") for r in rows],
|
||||
"items": items,
|
||||
"total": total, "page": page, "per_page": per_page,
|
||||
}
|
||||
|
||||
@@ -217,6 +292,55 @@ async def stats(
|
||||
}
|
||||
|
||||
|
||||
# --------------------------------------------------------------------------- #
|
||||
# LLM model list — fetch available models from the LLM provider
|
||||
# --------------------------------------------------------------------------- #
|
||||
@router.post("/llm/models")
|
||||
async def list_llm_models(
|
||||
api_url: str | None = None,
|
||||
api_key: str | None = None,
|
||||
db: AsyncSession = Depends(get_db),
|
||||
_user: User = Depends(require_admin),
|
||||
) -> dict:
|
||||
"""Fetch the list of available models from an OpenAI-compatible API.
|
||||
|
||||
Returns {ok: true, models: ["model1", "model2", ...]} on success.
|
||||
Returns {ok: false, error: {...}} on failure (no auto-fetch available).
|
||||
"""
|
||||
import httpx
|
||||
|
||||
settings = await get_all_settings(db)
|
||||
api_url = _resolve(api_url, settings.get("llm.api_url", ""))
|
||||
api_key = _resolve(api_key, settings.get("llm.api_key", ""))
|
||||
if not api_url:
|
||||
return {"ok": False, "error": {"code": "not_configured",
|
||||
"message": "llm.api_url is empty"},
|
||||
"models": []}
|
||||
try:
|
||||
async with httpx.AsyncClient(timeout=10.0) as client:
|
||||
resp = await client.get(
|
||||
f"{api_url.rstrip('/')}/models",
|
||||
headers={"Authorization": f"Bearer {api_key}"} if api_key else {},
|
||||
)
|
||||
if resp.status_code >= 400:
|
||||
return {"ok": False,
|
||||
"error": {"code": "api_error",
|
||||
"message": f"HTTP {resp.status_code}: {resp.text[:200]}"},
|
||||
"models": []}
|
||||
data = resp.json()
|
||||
models = []
|
||||
for m in data.get("data", []):
|
||||
mid = m.get("id") or m.get("name")
|
||||
if mid:
|
||||
models.append(mid)
|
||||
models.sort()
|
||||
return {"ok": True, "models": models, "count": len(models)}
|
||||
except Exception as e: # noqa: BLE001
|
||||
return {"ok": False,
|
||||
"error": {"code": "connection_failed", "message": str(e)},
|
||||
"models": []}
|
||||
|
||||
|
||||
# --------------------------------------------------------------------------- #
|
||||
# Helpers for test endpoints
|
||||
# --------------------------------------------------------------------------- #
|
||||
|
||||
@@ -76,10 +76,14 @@ async def get_state(
|
||||
next_actions = recent_steps[-1]["suggested_actions"] if recent_steps else []
|
||||
if world.intro_scene and not recent_steps:
|
||||
next_actions = []
|
||||
from app.core.time_utils import format_time_human
|
||||
return {
|
||||
"world": {
|
||||
"id": str(world.id), "name": world.name, "current_time": world.current_time,
|
||||
"id": str(world.id), "name": world.name,
|
||||
"current_time": world.current_time,
|
||||
"current_time_human": format_time_human(world.current_time, world.language),
|
||||
"language": world.language, "intro_scene": world.intro_scene,
|
||||
"status": world.status,
|
||||
},
|
||||
"environment": world.environment,
|
||||
"recent_steps": recent_steps,
|
||||
|
||||
@@ -69,11 +69,13 @@ async def list_worlds(
|
||||
env = w.environment or {}
|
||||
player = env.get("player") if isinstance(env, dict) else None
|
||||
pname = player.get("name") if isinstance(player, dict) else None
|
||||
from app.core.time_utils import format_time_human
|
||||
items.append({
|
||||
"id": str(w.id), "name": w.name, "description": w.description,
|
||||
"language": w.language, "status": w.status,
|
||||
"last_played_at": w.last_played_at.isoformat() if w.last_played_at else None,
|
||||
"current_time": w.current_time,
|
||||
"current_time_human": format_time_human(w.current_time, w.language),
|
||||
"created_at": w.created_at.isoformat(),
|
||||
"preview_player_name": pname,
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user