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:
@@ -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")
|
||||
Reference in New Issue
Block a user