c397740748
- WeChat Pay APIv3 integration (JSAPI + Native) with cert-based auth - TranslationQuota model + admin management UI (配额 tab) - Alibaba MT provider now checks quota before translation - Fix: admin tabs scrollable on mobile, remove header-card - Fix: profile/login navigation - logout stays on profile, login returns to profile - Fix: login form now visible by default (no extra click to show) - Fix: home page translate link uses navigateTo (was switchTab to non-tabBar page) - Add .coverage and apiclient_key.pem to gitignore
215 lines
6.3 KiB
Python
215 lines
6.3 KiB
Python
import uuid
|
|
from typing import Optional
|
|
from datetime import date, datetime
|
|
from fastapi import APIRouter, Depends, HTTPException, Query
|
|
from sqlalchemy.ext.asyncio import AsyncSession
|
|
from app.database import get_db
|
|
from app.services.admin import AdminService
|
|
from app.services.translation_quota import TranslationQuotaService
|
|
from app.api.v1.deps import get_current_user
|
|
|
|
router = APIRouter()
|
|
|
|
|
|
async def require_admin(current_user: dict = Depends(get_current_user)) -> dict:
|
|
if current_user.get("role") != "admin":
|
|
raise HTTPException(status_code=403, detail="Admin access required")
|
|
return current_user
|
|
|
|
|
|
@router.get("/dashboard")
|
|
async def get_dashboard(
|
|
_: dict = Depends(require_admin),
|
|
db: AsyncSession = Depends(get_db),
|
|
):
|
|
service = AdminService(db)
|
|
return await service.get_dashboard()
|
|
|
|
|
|
@router.get("/users")
|
|
async def list_users(
|
|
page: int = Query(1, ge=1),
|
|
size: int = Query(20, ge=1, le=100),
|
|
role: Optional[str] = Query(None),
|
|
_: dict = Depends(require_admin),
|
|
db: AsyncSession = Depends(get_db),
|
|
):
|
|
service = AdminService(db)
|
|
return await service.list_users(page, size, role)
|
|
|
|
|
|
def _validate_uuid(user_id: str):
|
|
try:
|
|
uuid.UUID(user_id)
|
|
except ValueError:
|
|
raise HTTPException(status_code=400, detail="Invalid user ID format")
|
|
|
|
|
|
@router.patch("/users/{target_user_id}/tier")
|
|
async def update_user_tier(
|
|
target_user_id: str,
|
|
data: dict,
|
|
_: dict = Depends(require_admin),
|
|
db: AsyncSession = Depends(get_db),
|
|
):
|
|
_validate_uuid(target_user_id)
|
|
service = AdminService(db)
|
|
tier = data.get("tier")
|
|
if tier not in ("free", "pro", "enterprise"):
|
|
raise HTTPException(status_code=400, detail="Invalid tier")
|
|
success = await service.update_user_tier(target_user_id, tier)
|
|
if not success:
|
|
raise HTTPException(status_code=404, detail="User not found")
|
|
return {"message": f"User tier updated to {tier}"}
|
|
|
|
|
|
@router.post("/users/{target_user_id}/toggle-active")
|
|
async def toggle_user_active(
|
|
target_user_id: str,
|
|
_: dict = Depends(require_admin),
|
|
db: AsyncSession = Depends(get_db),
|
|
):
|
|
_validate_uuid(target_user_id)
|
|
service = AdminService(db)
|
|
success = await service.toggle_user_active(target_user_id)
|
|
if not success:
|
|
raise HTTPException(status_code=404, detail="User not found")
|
|
return {"message": "User active status toggled"}
|
|
|
|
|
|
@router.patch("/users/{target_user_id}/role")
|
|
async def update_user_role(
|
|
target_user_id: str,
|
|
data: dict,
|
|
_: dict = Depends(require_admin),
|
|
db: AsyncSession = Depends(get_db),
|
|
):
|
|
_validate_uuid(target_user_id)
|
|
service = AdminService(db)
|
|
role = data.get("role")
|
|
if role not in ("user", "admin"):
|
|
raise HTTPException(status_code=400, detail="Invalid role. Must be 'user' or 'admin'")
|
|
result = await service.update_user_role(target_user_id, role)
|
|
if not result:
|
|
raise HTTPException(status_code=404, detail="User not found")
|
|
return result
|
|
|
|
|
|
@router.get("/users/search")
|
|
async def search_users(
|
|
q: str = Query(..., min_length=1),
|
|
_: dict = Depends(require_admin),
|
|
db: AsyncSession = Depends(get_db),
|
|
):
|
|
service = AdminService(db)
|
|
return await service.search_users(q)
|
|
|
|
|
|
@router.get("/users/{target_user_id}")
|
|
async def get_user_detail(
|
|
target_user_id: str,
|
|
_: dict = Depends(require_admin),
|
|
db: AsyncSession = Depends(get_db),
|
|
):
|
|
_validate_uuid(target_user_id)
|
|
service = AdminService(db)
|
|
result = await service.get_user_detail(target_user_id)
|
|
if not result:
|
|
raise HTTPException(status_code=404, detail="User not found")
|
|
return result
|
|
|
|
|
|
@router.get("/usage-stats")
|
|
async def get_usage_stats(
|
|
_: dict = Depends(require_admin),
|
|
db: AsyncSession = Depends(get_db),
|
|
):
|
|
service = AdminService(db)
|
|
return await service.get_usage_stats()
|
|
|
|
|
|
@router.get("/logs")
|
|
async def get_logs(
|
|
page: int = Query(1, ge=1),
|
|
size: int = Query(50, ge=1, le=200),
|
|
action: Optional[str] = Query(None),
|
|
user_id: Optional[str] = Query(None),
|
|
date_from: Optional[date] = Query(None),
|
|
date_to: Optional[date] = Query(None),
|
|
_: dict = Depends(require_admin),
|
|
db: AsyncSession = Depends(get_db),
|
|
):
|
|
service = AdminService(db)
|
|
dt_from = datetime.combine(date_from, datetime.min.time()) if date_from else None
|
|
dt_to = datetime.combine(date_to, datetime.max.time()) if date_to else None
|
|
return await service.get_logs(page, size, action, user_id, dt_from, dt_to)
|
|
|
|
|
|
@router.get("/config")
|
|
async def list_config(
|
|
_: dict = Depends(require_admin),
|
|
db: AsyncSession = Depends(get_db),
|
|
):
|
|
service = AdminService(db)
|
|
return await service.list_config()
|
|
|
|
|
|
@router.put("/config/{key}")
|
|
async def update_config(
|
|
key: str,
|
|
data: dict,
|
|
_: dict = Depends(require_admin),
|
|
db: AsyncSession = Depends(get_db),
|
|
):
|
|
service = AdminService(db)
|
|
item = await service.update_config(key, data.get("value"))
|
|
if not item:
|
|
raise HTTPException(status_code=404, detail="Config not found")
|
|
return item
|
|
|
|
|
|
@router.get("/health")
|
|
async def system_health(
|
|
db: AsyncSession = Depends(get_db),
|
|
):
|
|
service = AdminService(db)
|
|
return await service.get_system_health()
|
|
|
|
|
|
@router.get("/translation-quotas")
|
|
async def list_translation_quotas(
|
|
_: dict = Depends(require_admin),
|
|
db: AsyncSession = Depends(get_db),
|
|
):
|
|
service = TranslationQuotaService(db)
|
|
return await service.get_all_quotas()
|
|
|
|
|
|
@router.put("/translation-quotas/{version}")
|
|
async def update_translation_quota(
|
|
version: str,
|
|
data: dict,
|
|
_: dict = Depends(require_admin),
|
|
db: AsyncSession = Depends(get_db),
|
|
):
|
|
allowed = {"monthly_limit", "enabled", "description"}
|
|
filtered = {k: v for k, v in data.items() if k in allowed}
|
|
service = TranslationQuotaService(db)
|
|
result = await service.update_quota(version, filtered)
|
|
if not result:
|
|
raise HTTPException(status_code=404, detail="Quota not found")
|
|
return result
|
|
|
|
|
|
@router.post("/translation-quotas/{version}/reset")
|
|
async def reset_translation_quota(
|
|
version: str,
|
|
_: dict = Depends(require_admin),
|
|
db: AsyncSession = Depends(get_db),
|
|
):
|
|
service = TranslationQuotaService(db)
|
|
result = await service.reset_usage(version)
|
|
if not result:
|
|
raise HTTPException(status_code=404, detail="Quota not found")
|
|
return result
|