Add landing page, referral system, usage quotas, search API management, and yearly pricing

- Separate workspace landing from login for better UX
- Referral system rewards both parties with Pro days
- Quota enforcement prevents abuse without breaking endpoints
- 7-day free trial with auto-downgrade on expiry
- Admin-managed search provider config (SearXNG, Bing)
- 15% discount on annual subscriptions
- MCP search server wrapping opencode search
- Fix discovery module field name mismatch causing 422
This commit is contained in:
TradeMate Dev
2026-05-26 11:40:13 +08:00
parent 52dba37f22
commit bed5c7abef
39 changed files with 1988 additions and 152 deletions
+189
View File
@@ -0,0 +1,189 @@
from typing import Optional
from pydantic import BaseModel
from fastapi import APIRouter, Depends, HTTPException, Query
from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy import select, delete
from app.database import get_db
from app.api.v1.deps import get_current_user
from app.models.search_provider import SearchProvider
from app.services.search import SearchService
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
class ProviderCreate(BaseModel):
name: str
provider_type: str
api_key: Optional[str] = None
api_endpoint: Optional[str] = None
extra_config: Optional[dict] = None
priority: int = 0
enabled: bool = True
class ProviderUpdate(BaseModel):
name: Optional[str] = None
api_key: Optional[str] = None
api_endpoint: Optional[str] = None
extra_config: Optional[dict] = None
priority: Optional[int] = None
enabled: Optional[bool] = None
@router.get("/search-providers")
async def list_providers(
page: int = Query(1, ge=1),
size: int = Query(50, ge=1, le=100),
_: dict = Depends(require_admin),
db: AsyncSession = Depends(get_db),
):
result = await db.execute(
select(SearchProvider).order_by(SearchProvider.priority).offset((page - 1) * size).limit(size)
)
providers = result.scalars().all()
total_result = await db.execute(select(SearchProvider))
total = len(total_result.scalars().all())
return {
"items": [
{
"id": str(p.id),
"name": p.name,
"provider_type": p.provider_type,
"api_key": p.api_key[:8] + "..." if p.api_key and len(p.api_key) > 8 else p.api_key,
"api_endpoint": p.api_endpoint,
"extra_config": p.extra_config,
"priority": p.priority,
"enabled": p.enabled,
"created_at": p.created_at.isoformat() if p.created_at else None,
"updated_at": p.updated_at.isoformat() if p.updated_at else None,
}
for p in providers
],
"total": total,
"page": page,
"size": size,
}
@router.post("/search-providers")
async def create_provider(
data: ProviderCreate,
_: dict = Depends(require_admin),
db: AsyncSession = Depends(get_db),
):
provider = SearchProvider(
name=data.name,
provider_type=data.provider_type,
api_key=data.api_key,
api_endpoint=data.api_endpoint,
extra_config=data.extra_config or {},
priority=data.priority,
enabled=data.enabled,
)
db.add(provider)
await db.flush()
return {
"id": str(provider.id),
"name": provider.name,
"provider_type": provider.provider_type,
"message": "Provider created",
}
@router.get("/search-providers/{provider_id}")
async def get_provider(
provider_id: str,
_: dict = Depends(require_admin),
db: AsyncSession = Depends(get_db),
):
_validate_uuid(provider_id)
result = await db.execute(select(SearchProvider).where(SearchProvider.id == provider_id))
p = result.scalar_one_or_none()
if not p:
raise HTTPException(status_code=404, detail="Provider not found")
return {
"id": str(p.id),
"name": p.name,
"provider_type": p.provider_type,
"api_key": p.api_key,
"api_endpoint": p.api_endpoint,
"extra_config": p.extra_config,
"priority": p.priority,
"enabled": p.enabled,
"created_at": p.created_at.isoformat() if p.created_at else None,
"updated_at": p.updated_at.isoformat() if p.updated_at else None,
}
@router.put("/search-providers/{provider_id}")
async def update_provider(
provider_id: str,
data: ProviderUpdate,
_: dict = Depends(require_admin),
db: AsyncSession = Depends(get_db),
):
_validate_uuid(provider_id)
result = await db.execute(select(SearchProvider).where(SearchProvider.id == provider_id))
p = result.scalar_one_or_none()
if not p:
raise HTTPException(status_code=404, detail="Provider not found")
if data.name is not None:
p.name = data.name
if data.api_key is not None:
p.api_key = data.api_key
if data.api_endpoint is not None:
p.api_endpoint = data.api_endpoint
if data.extra_config is not None:
p.extra_config = data.extra_config
if data.priority is not None:
p.priority = data.priority
if data.enabled is not None:
p.enabled = data.enabled
await db.flush()
return {"message": "Provider updated"}
@router.delete("/search-providers/{provider_id}")
async def delete_provider(
provider_id: str,
_: dict = Depends(require_admin),
db: AsyncSession = Depends(get_db),
):
_validate_uuid(provider_id)
result = await db.execute(delete(SearchProvider).where(SearchProvider.id == provider_id))
if result.rowcount == 0:
raise HTTPException(status_code=404, detail="Provider not found")
return {"message": "Provider deleted"}
@router.post("/search-providers/{provider_id}/test")
async def test_provider(
provider_id: str,
_: dict = Depends(require_admin),
db: AsyncSession = Depends(get_db),
):
_validate_uuid(provider_id)
result = await db.execute(select(SearchProvider).where(SearchProvider.id == provider_id))
p = result.scalar_one_or_none()
if not p:
raise HTTPException(status_code=404, detail="Provider not found")
try:
svc = SearchService(db)
results = await svc._search_provider(p, "test", 3)
return {"success": True, "results": results}
except Exception as e:
return {"success": False, "error": str(e)}
def _validate_uuid(uuid_str: str):
import uuid
try:
uuid.UUID(uuid_str)
except ValueError:
raise HTTPException(status_code=400, detail="Invalid UUID")