Files
remnawave-bedolaga-telegram…/app/cabinet/routes/admin_settings.py
PEDZEO 6b69ec750e feat: add cabinet (personal account) backend API
- Add JWT authentication for cabinet users
- Add Telegram WebApp authentication
- Add subscription management endpoints
- Add balance and transactions endpoints
- Add referral system endpoints
- Add tickets support for cabinet
- Add webhooks and websocket for real-time updates
- Add email verification service

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-01 23:20:20 +03:00

265 lines
8.2 KiB
Python

"""Admin settings routes for cabinet - system configuration management."""
import logging
from typing import Any, Optional, List
from fastapi import APIRouter, Depends, HTTPException, status, Query
from pydantic import BaseModel, Field
from sqlalchemy.ext.asyncio import AsyncSession
from app.database.models import User
from app.services.system_settings_service import (
ReadOnlySettingError,
bot_configuration_service,
)
from ..dependencies import get_cabinet_db, get_current_admin_user
logger = logging.getLogger(__name__)
router = APIRouter(prefix="/admin/settings", tags=["Admin Settings"])
# ============ Schemas ============
class SettingCategoryRef(BaseModel):
"""Reference to category."""
key: str
label: str
class SettingCategorySummary(BaseModel):
"""Category summary."""
key: str
label: str
description: str = ""
items: int
class SettingChoice(BaseModel):
"""Choice option for setting."""
value: Any
label: str
description: Optional[str] = None
class SettingHint(BaseModel):
"""Setting hints and guidance."""
description: str = ""
format: str = ""
example: str = ""
warning: str = ""
class SettingDefinition(BaseModel):
"""Full setting definition with current state."""
key: str
name: str
category: SettingCategoryRef
type: str
is_optional: bool
current: Any = Field(default=None)
original: Any = Field(default=None)
has_override: bool
read_only: bool = Field(default=False)
choices: List[SettingChoice] = Field(default_factory=list)
hint: Optional[SettingHint] = None
class SettingUpdateRequest(BaseModel):
"""Request to update setting value."""
value: Any
# ============ Helper Functions ============
def _coerce_value(key: str, value: Any) -> Any:
"""Convert and validate value for a setting."""
definition = bot_configuration_service.get_definition(key)
if value is None:
if definition.is_optional:
return None
raise HTTPException(status.HTTP_400_BAD_REQUEST, "Value is required")
python_type = definition.python_type
try:
if python_type is bool:
if isinstance(value, bool):
normalized = value
elif isinstance(value, str):
lowered = value.strip().lower()
if lowered in {"true", "1", "yes", "on", "да"}:
normalized = True
elif lowered in {"false", "0", "no", "off", "нет"}:
normalized = False
else:
raise ValueError("invalid bool")
else:
raise ValueError("invalid bool")
elif python_type is int:
normalized = int(value)
elif python_type is float:
normalized = float(value)
else:
normalized = str(value)
except ValueError:
raise HTTPException(status.HTTP_400_BAD_REQUEST, "Invalid value type") from None
choices = bot_configuration_service.get_choice_options(key)
if choices:
allowed_values = {option.value for option in choices}
if normalized not in allowed_values:
readable = ", ".join(bot_configuration_service.format_value(opt.value) for opt in choices)
raise HTTPException(
status.HTTP_400_BAD_REQUEST,
detail=f"Value must be one of: {readable}",
)
return normalized
def _serialize_definition(definition, include_choices: bool = True) -> SettingDefinition:
"""Serialize setting definition to response model."""
current = bot_configuration_service.get_current_value(definition.key)
original = bot_configuration_service.get_original_value(definition.key)
has_override = bot_configuration_service.has_override(definition.key)
choices: List[SettingChoice] = []
if include_choices:
choices = [
SettingChoice(
value=option.value,
label=option.label,
description=option.description,
)
for option in bot_configuration_service.get_choice_options(definition.key)
]
# Get setting hints
guidance = bot_configuration_service.get_setting_guidance(definition.key)
hint = SettingHint(
description=guidance.get("description", ""),
format=guidance.get("format", ""),
example=guidance.get("example", ""),
warning=guidance.get("warning", ""),
)
return SettingDefinition(
key=definition.key,
name=definition.display_name,
category=SettingCategoryRef(
key=definition.category_key,
label=definition.category_label,
),
type=definition.type_label,
is_optional=definition.is_optional,
current=current,
original=original,
has_override=has_override,
read_only=bot_configuration_service.is_read_only(definition.key),
choices=choices,
hint=hint,
)
# ============ Routes ============
@router.get("/categories", response_model=List[SettingCategorySummary])
async def list_categories(
admin: User = Depends(get_current_admin_user),
):
"""Get list of setting categories."""
categories = bot_configuration_service.get_categories()
return [
SettingCategorySummary(
key=key,
label=label,
description=bot_configuration_service.get_category_description(key),
items=count,
)
for key, label, count in categories
]
@router.get("", response_model=List[SettingDefinition])
async def list_settings(
admin: User = Depends(get_current_admin_user),
category: Optional[str] = Query(default=None, alias="category_key"),
):
"""Get list of all settings or settings for a specific category."""
items: List[SettingDefinition] = []
if category:
definitions = bot_configuration_service.get_settings_for_category(category)
items.extend(_serialize_definition(defn) for defn in definitions)
return items
for category_key, _, _ in bot_configuration_service.get_categories():
definitions = bot_configuration_service.get_settings_for_category(category_key)
items.extend(_serialize_definition(defn) for defn in definitions)
return items
@router.get("/{key}", response_model=SettingDefinition)
async def get_setting(
key: str,
admin: User = Depends(get_current_admin_user),
):
"""Get a specific setting by key."""
try:
definition = bot_configuration_service.get_definition(key)
except KeyError as error:
raise HTTPException(status.HTTP_404_NOT_FOUND, "Setting not found") from error
return _serialize_definition(definition)
@router.put("/{key}", response_model=SettingDefinition)
async def update_setting(
key: str,
payload: SettingUpdateRequest,
admin: User = Depends(get_current_admin_user),
db: AsyncSession = Depends(get_cabinet_db),
):
"""Update a setting value."""
try:
definition = bot_configuration_service.get_definition(key)
except KeyError as error:
raise HTTPException(status.HTTP_404_NOT_FOUND, "Setting not found") from error
value = _coerce_value(key, payload.value)
try:
await bot_configuration_service.set_value(db, key, value)
except ReadOnlySettingError as error:
raise HTTPException(status.HTTP_403_FORBIDDEN, str(error)) from error
await db.commit()
logger.info(f"Admin {admin.telegram_id} updated setting {key} to {value}")
return _serialize_definition(definition)
@router.delete("/{key}", response_model=SettingDefinition)
async def reset_setting(
key: str,
admin: User = Depends(get_current_admin_user),
db: AsyncSession = Depends(get_cabinet_db),
):
"""Reset a setting to its default value."""
try:
definition = bot_configuration_service.get_definition(key)
except KeyError as error:
raise HTTPException(status.HTTP_404_NOT_FOUND, "Setting not found") from error
try:
await bot_configuration_service.reset_value(db, key)
except ReadOnlySettingError as error:
raise HTTPException(status.HTTP_403_FORBIDDEN, str(error)) from error
await db.commit()
logger.info(f"Admin {admin.telegram_id} reset setting {key}")
return _serialize_definition(definition)