13e3992d4c
Security fixes: - Add file upload size limits (10MB) for customer and product imports - Add XLSX file validation with row limits and magic byte checking - Implement password validation (min 6 chars) in registration - Add rate limiting for guest login (5 per IP per 15 minutes) - Sanitize error messages to prevent information leakage - Fix XSS vulnerability by removing unsafe v-html usage - Enforce WhatsApp webhook signature verification - Add SSRF protection with URL validation and IP blocking - Fix marketing endpoints to use proper authentication Code quality improvements: - Create shared utility functions for UUID validation and string sanitization - Remove duplicate UUID validation code from admin modules - Remove dead code (pass statement in translation.py) - Fix aliyun SDK import compatibility
344 lines
10 KiB
Python
344 lines
10 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.services.certification import CertificationService
|
|
from app.services.invoice import InvoiceService
|
|
from app.services.payment import PaymentService
|
|
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)
|
|
|
|
|
|
from app.core.utils import validate_uuid
|
|
|
|
|
|
def _validate_uuid(user_id: str):
|
|
validate_uuid(user_id)
|
|
|
|
|
|
@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
|
|
|
|
|
|
@router.get("/certifications")
|
|
async def admin_list_certifications(
|
|
page: int = Query(1, ge=1),
|
|
size: int = Query(20, ge=1, le=100),
|
|
status: Optional[str] = Query(None),
|
|
_: dict = Depends(require_admin),
|
|
db: AsyncSession = Depends(get_db),
|
|
):
|
|
service = CertificationService(db)
|
|
return await service.list_all(page, size, status)
|
|
|
|
|
|
@router.post("/certifications/{cert_id}/review")
|
|
async def admin_review_certification(
|
|
cert_id: str,
|
|
data: dict,
|
|
_: dict = Depends(require_admin),
|
|
db: AsyncSession = Depends(get_db),
|
|
):
|
|
_validate_uuid(cert_id)
|
|
service = CertificationService(db)
|
|
action = data.get("action")
|
|
if action not in ("approve", "reject"):
|
|
raise HTTPException(status_code=400, detail="Action must be 'approve' or 'reject'")
|
|
result = await service.review(cert_id, action, data.get("reason"))
|
|
if not result:
|
|
raise HTTPException(status_code=404, detail="Certification not found")
|
|
return result
|
|
|
|
|
|
@router.get("/invoices")
|
|
async def admin_list_invoices(
|
|
page: int = Query(1, ge=1),
|
|
size: int = Query(20, ge=1, le=100),
|
|
status: Optional[str] = Query(None),
|
|
_: dict = Depends(require_admin),
|
|
db: AsyncSession = Depends(get_db),
|
|
):
|
|
service = InvoiceService(db)
|
|
return await service.list_all(page, size, status)
|
|
|
|
|
|
@router.post("/invoices/{invoice_id}/process")
|
|
async def admin_process_invoice(
|
|
invoice_id: str,
|
|
data: dict,
|
|
_: dict = Depends(require_admin),
|
|
db: AsyncSession = Depends(get_db),
|
|
):
|
|
_validate_uuid(invoice_id)
|
|
service = InvoiceService(db)
|
|
action = data.get("action")
|
|
if action not in ("issue", "reject"):
|
|
raise HTTPException(status_code=400, detail="Action must be 'issue' or 'reject'")
|
|
result = await service.process(invoice_id, action, data.get("reason"))
|
|
if not result:
|
|
raise HTTPException(status_code=404, detail="Invoice not found")
|
|
return result
|
|
|
|
|
|
@router.get("/payments")
|
|
async def admin_list_payments(
|
|
page: int = Query(1, ge=1),
|
|
size: int = Query(20, ge=1, le=100),
|
|
gateway: str = Query(default=""),
|
|
status: str = Query(default=""),
|
|
pay_type: str = Query(default=""),
|
|
user_id: str = Query(default=""),
|
|
_: dict = Depends(require_admin),
|
|
db: AsyncSession = Depends(get_db),
|
|
):
|
|
svc = PaymentService(db)
|
|
return await svc.admin_list_payments(page, size, gateway, status, user_id, pay_type)
|
|
|
|
|
|
@router.get("/payments/stats")
|
|
async def admin_payment_stats(
|
|
_: dict = Depends(require_admin),
|
|
db: AsyncSession = Depends(get_db),
|
|
):
|
|
svc = PaymentService(db)
|
|
return await svc.admin_payment_stats()
|
|
|
|
|
|
@router.post("/payments/refund")
|
|
async def admin_refund(
|
|
data: dict,
|
|
_: dict = Depends(require_admin),
|
|
db: AsyncSession = Depends(get_db),
|
|
):
|
|
order_no = data.get("order_no", "")
|
|
reason = data.get("reason", "")
|
|
svc = PaymentService(db)
|
|
try:
|
|
return await svc.admin_refund(order_no, reason)
|
|
except ValueError as e:
|
|
raise HTTPException(status_code=400, detail=str(e))
|
|
|
|
|
|
@router.post("/payments/close")
|
|
async def admin_close_order(
|
|
data: dict,
|
|
_: dict = Depends(require_admin),
|
|
db: AsyncSession = Depends(get_db),
|
|
):
|
|
order_no = data.get("order_no", "")
|
|
svc = PaymentService(db)
|
|
try:
|
|
return await svc.admin_close_order(order_no)
|
|
except ValueError as e:
|
|
raise HTTPException(status_code=400, detail=str(e))
|
|
|
|
|
|
@router.get("/payments/query-refund/{order_no}")
|
|
async def admin_query_refund(
|
|
order_no: str,
|
|
_: dict = Depends(require_admin),
|
|
db: AsyncSession = Depends(get_db),
|
|
):
|
|
svc = PaymentService(db)
|
|
try:
|
|
return await svc.query_refund(order_no)
|
|
except ValueError as e:
|
|
raise HTTPException(status_code=404, detail=str(e))
|