feat: 添加API Key认证功能 [AC-AISVC-50]
- 新增 ApiKey 模型和数据库表 - 新增 ApiKeyService 服务,支持内存缓存验证 - 新增 ApiKeyMiddleware 中间件,验证所有请求 - 应用启动时自动创建默认 API Key - 新增 /admin/api-keys 管理接口
This commit is contained in:
parent
77033efd34
commit
ee2c7c0d0c
|
|
@ -1,8 +1,9 @@
|
||||||
"""
|
"""
|
||||||
Admin API routes for AI Service management.
|
Admin API routes for AI Service management.
|
||||||
[AC-ASA-01, AC-ASA-02, AC-ASA-05, AC-ASA-07, AC-ASA-08] Admin management endpoints.
|
[AC-ASA-01, AC-ASA-02, AC-ASA-05, AC-ASA-07, AC-ASA-08, AC-AISVC-50] Admin management endpoints.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
from app.api.admin.api_key import router as api_key_router
|
||||||
from app.api.admin.dashboard import router as dashboard_router
|
from app.api.admin.dashboard import router as dashboard_router
|
||||||
from app.api.admin.embedding import router as embedding_router
|
from app.api.admin.embedding import router as embedding_router
|
||||||
from app.api.admin.kb import router as kb_router
|
from app.api.admin.kb import router as kb_router
|
||||||
|
|
@ -11,4 +12,4 @@ from app.api.admin.rag import router as rag_router
|
||||||
from app.api.admin.sessions import router as sessions_router
|
from app.api.admin.sessions import router as sessions_router
|
||||||
from app.api.admin.tenants import router as tenants_router
|
from app.api.admin.tenants import router as tenants_router
|
||||||
|
|
||||||
__all__ = ["dashboard_router", "embedding_router", "kb_router", "llm_router", "rag_router", "sessions_router", "tenants_router"]
|
__all__ = ["api_key_router", "dashboard_router", "embedding_router", "kb_router", "llm_router", "rag_router", "sessions_router", "tenants_router"]
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,154 @@
|
||||||
|
"""
|
||||||
|
API Key management endpoints.
|
||||||
|
[AC-AISVC-50] CRUD operations for API keys.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import logging
|
||||||
|
from typing import Annotated
|
||||||
|
|
||||||
|
from fastapi import APIRouter, Depends, HTTPException, status
|
||||||
|
from pydantic import BaseModel, Field
|
||||||
|
from sqlalchemy.ext.asyncio import AsyncSession
|
||||||
|
|
||||||
|
from app.core.database import get_session
|
||||||
|
from app.models.entities import ApiKey, ApiKeyCreate
|
||||||
|
from app.services.api_key import get_api_key_service
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
router = APIRouter(prefix="/admin/api-keys", tags=["API Keys"])
|
||||||
|
|
||||||
|
|
||||||
|
class ApiKeyResponse(BaseModel):
|
||||||
|
"""Response model for API key."""
|
||||||
|
|
||||||
|
id: str = Field(..., description="API key ID")
|
||||||
|
key: str = Field(..., description="API key value")
|
||||||
|
name: str = Field(..., description="API key name")
|
||||||
|
is_active: bool = Field(..., description="Whether the key is active")
|
||||||
|
created_at: str = Field(..., description="Creation time")
|
||||||
|
updated_at: str = Field(..., description="Last update time")
|
||||||
|
|
||||||
|
|
||||||
|
class ApiKeyListResponse(BaseModel):
|
||||||
|
"""Response model for API key list."""
|
||||||
|
|
||||||
|
keys: list[ApiKeyResponse] = Field(..., description="List of API keys")
|
||||||
|
total: int = Field(..., description="Total count")
|
||||||
|
|
||||||
|
|
||||||
|
class CreateApiKeyRequest(BaseModel):
|
||||||
|
"""Request model for creating API key."""
|
||||||
|
|
||||||
|
name: str = Field(..., description="API key name/description")
|
||||||
|
key: str | None = Field(default=None, description="Custom API key (auto-generated if not provided)")
|
||||||
|
|
||||||
|
|
||||||
|
class ToggleApiKeyRequest(BaseModel):
|
||||||
|
"""Request model for toggling API key status."""
|
||||||
|
|
||||||
|
is_active: bool = Field(..., description="New active status")
|
||||||
|
|
||||||
|
|
||||||
|
def api_key_to_response(api_key: ApiKey) -> ApiKeyResponse:
|
||||||
|
"""Convert ApiKey entity to response model."""
|
||||||
|
return ApiKeyResponse(
|
||||||
|
id=str(api_key.id),
|
||||||
|
key=api_key.key,
|
||||||
|
name=api_key.name,
|
||||||
|
is_active=api_key.is_active,
|
||||||
|
created_at=api_key.created_at.isoformat(),
|
||||||
|
updated_at=api_key.updated_at.isoformat(),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@router.get("", response_model=ApiKeyListResponse)
|
||||||
|
async def list_api_keys(
|
||||||
|
session: Annotated[AsyncSession, Depends(get_session)],
|
||||||
|
):
|
||||||
|
"""
|
||||||
|
[AC-AISVC-50] List all API keys.
|
||||||
|
"""
|
||||||
|
service = get_api_key_service()
|
||||||
|
keys = await service.list_keys(session)
|
||||||
|
|
||||||
|
return ApiKeyListResponse(
|
||||||
|
keys=[api_key_to_response(k) for k in keys],
|
||||||
|
total=len(keys),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@router.post("", response_model=ApiKeyResponse, status_code=status.HTTP_201_CREATED)
|
||||||
|
async def create_api_key(
|
||||||
|
request: CreateApiKeyRequest,
|
||||||
|
session: Annotated[AsyncSession, Depends(get_session)],
|
||||||
|
):
|
||||||
|
"""
|
||||||
|
[AC-AISVC-50] Create a new API key.
|
||||||
|
"""
|
||||||
|
service = get_api_key_service()
|
||||||
|
|
||||||
|
key_value = request.key or service.generate_key()
|
||||||
|
|
||||||
|
key_create = ApiKeyCreate(
|
||||||
|
key=key_value,
|
||||||
|
name=request.name,
|
||||||
|
is_active=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
api_key = await service.create_key(session, key_create)
|
||||||
|
logger.info(f"[AC-AISVC-50] Created API key: {api_key.name}")
|
||||||
|
|
||||||
|
return api_key_to_response(api_key)
|
||||||
|
|
||||||
|
|
||||||
|
@router.delete("/{key_id}", status_code=status.HTTP_204_NO_CONTENT)
|
||||||
|
async def delete_api_key(
|
||||||
|
key_id: str,
|
||||||
|
session: Annotated[AsyncSession, Depends(get_session)],
|
||||||
|
):
|
||||||
|
"""
|
||||||
|
[AC-AISVC-50] Delete an API key.
|
||||||
|
"""
|
||||||
|
service = get_api_key_service()
|
||||||
|
|
||||||
|
deleted = await service.delete_key(session, key_id)
|
||||||
|
|
||||||
|
if not deleted:
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=status.HTTP_404_NOT_FOUND,
|
||||||
|
detail="API key not found",
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@router.patch("/{key_id}/toggle", response_model=ApiKeyResponse)
|
||||||
|
async def toggle_api_key(
|
||||||
|
key_id: str,
|
||||||
|
request: ToggleApiKeyRequest,
|
||||||
|
session: Annotated[AsyncSession, Depends(get_session)],
|
||||||
|
):
|
||||||
|
"""
|
||||||
|
[AC-AISVC-50] Toggle API key active status.
|
||||||
|
"""
|
||||||
|
service = get_api_key_service()
|
||||||
|
|
||||||
|
api_key = await service.toggle_key(session, key_id, request.is_active)
|
||||||
|
|
||||||
|
if not api_key:
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=status.HTTP_404_NOT_FOUND,
|
||||||
|
detail="API key not found",
|
||||||
|
)
|
||||||
|
|
||||||
|
return api_key_to_response(api_key)
|
||||||
|
|
||||||
|
|
||||||
|
@router.post("/reload-cache", status_code=status.HTTP_204_NO_CONTENT)
|
||||||
|
async def reload_api_key_cache(
|
||||||
|
session: Annotated[AsyncSession, Depends(get_session)],
|
||||||
|
):
|
||||||
|
"""
|
||||||
|
[AC-AISVC-50] Reload API key cache from database.
|
||||||
|
"""
|
||||||
|
service = get_api_key_service()
|
||||||
|
await service.reload_cache(session)
|
||||||
|
|
@ -12,7 +12,7 @@ from fastapi.middleware.cors import CORSMiddleware
|
||||||
from fastapi.responses import JSONResponse
|
from fastapi.responses import JSONResponse
|
||||||
|
|
||||||
from app.api import chat_router, health_router
|
from app.api import chat_router, health_router
|
||||||
from app.api.admin import dashboard_router, embedding_router, kb_router, llm_router, rag_router, sessions_router, tenants_router
|
from app.api.admin import api_key_router, dashboard_router, embedding_router, kb_router, llm_router, rag_router, sessions_router, tenants_router
|
||||||
from app.api.admin.kb_optimized import router as kb_optimized_router
|
from app.api.admin.kb_optimized import router as kb_optimized_router
|
||||||
from app.core.config import get_settings
|
from app.core.config import get_settings
|
||||||
from app.core.database import close_db, init_db
|
from app.core.database import close_db, init_db
|
||||||
|
|
@ -24,7 +24,7 @@ from app.core.exceptions import (
|
||||||
generic_exception_handler,
|
generic_exception_handler,
|
||||||
http_exception_handler,
|
http_exception_handler,
|
||||||
)
|
)
|
||||||
from app.core.middleware import TenantContextMiddleware
|
from app.core.middleware import ApiKeyMiddleware, TenantContextMiddleware
|
||||||
from app.core.qdrant_client import close_qdrant_client
|
from app.core.qdrant_client import close_qdrant_client
|
||||||
|
|
||||||
settings = get_settings()
|
settings = get_settings()
|
||||||
|
|
@ -40,7 +40,7 @@ logger = logging.getLogger(__name__)
|
||||||
@asynccontextmanager
|
@asynccontextmanager
|
||||||
async def lifespan(app: FastAPI):
|
async def lifespan(app: FastAPI):
|
||||||
"""
|
"""
|
||||||
[AC-AISVC-01, AC-AISVC-11] Application lifespan manager.
|
[AC-AISVC-01, AC-AISVC-11, AC-AISVC-50] Application lifespan manager.
|
||||||
Handles startup and shutdown of database and external connections.
|
Handles startup and shutdown of database and external connections.
|
||||||
"""
|
"""
|
||||||
logger.info(f"[AC-AISVC-01] Starting {settings.app_name} v{settings.app_version}")
|
logger.info(f"[AC-AISVC-01] Starting {settings.app_name} v{settings.app_version}")
|
||||||
|
|
@ -51,6 +51,19 @@ async def lifespan(app: FastAPI):
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.warning(f"[AC-AISVC-11] Database initialization skipped: {e}")
|
logger.warning(f"[AC-AISVC-11] Database initialization skipped: {e}")
|
||||||
|
|
||||||
|
try:
|
||||||
|
from app.core.database import async_session_maker
|
||||||
|
from app.services.api_key import get_api_key_service
|
||||||
|
|
||||||
|
async with async_session_maker() as session:
|
||||||
|
api_key_service = get_api_key_service()
|
||||||
|
await api_key_service.initialize(session)
|
||||||
|
default_key = await api_key_service.create_default_key(session)
|
||||||
|
if default_key:
|
||||||
|
logger.info(f"[AC-AISVC-50] Default API key created: {default_key.key}")
|
||||||
|
except Exception as e:
|
||||||
|
logger.warning(f"[AC-AISVC-50] API key initialization skipped: {e}")
|
||||||
|
|
||||||
yield
|
yield
|
||||||
|
|
||||||
await close_db()
|
await close_db()
|
||||||
|
|
@ -87,6 +100,7 @@ app.add_middleware(
|
||||||
)
|
)
|
||||||
|
|
||||||
app.add_middleware(TenantContextMiddleware)
|
app.add_middleware(TenantContextMiddleware)
|
||||||
|
app.add_middleware(ApiKeyMiddleware)
|
||||||
|
|
||||||
app.add_exception_handler(AIServiceException, ai_service_exception_handler)
|
app.add_exception_handler(AIServiceException, ai_service_exception_handler)
|
||||||
app.add_exception_handler(HTTPException, http_exception_handler)
|
app.add_exception_handler(HTTPException, http_exception_handler)
|
||||||
|
|
@ -113,6 +127,7 @@ async def validation_exception_handler(request: Request, exc: RequestValidationE
|
||||||
app.include_router(health_router)
|
app.include_router(health_router)
|
||||||
app.include_router(chat_router)
|
app.include_router(chat_router)
|
||||||
|
|
||||||
|
app.include_router(api_key_router)
|
||||||
app.include_router(dashboard_router)
|
app.include_router(dashboard_router)
|
||||||
app.include_router(embedding_router)
|
app.include_router(embedding_router)
|
||||||
app.include_router(kb_router)
|
app.include_router(kb_router)
|
||||||
|
|
|
||||||
|
|
@ -50,6 +50,7 @@ class ErrorCode(str, Enum):
|
||||||
INVALID_REQUEST = "INVALID_REQUEST"
|
INVALID_REQUEST = "INVALID_REQUEST"
|
||||||
MISSING_TENANT_ID = "MISSING_TENANT_ID"
|
MISSING_TENANT_ID = "MISSING_TENANT_ID"
|
||||||
INVALID_TENANT_ID = "INVALID_TENANT_ID"
|
INVALID_TENANT_ID = "INVALID_TENANT_ID"
|
||||||
|
UNAUTHORIZED = "UNAUTHORIZED"
|
||||||
INTERNAL_ERROR = "INTERNAL_ERROR"
|
INTERNAL_ERROR = "INTERNAL_ERROR"
|
||||||
SERVICE_UNAVAILABLE = "SERVICE_UNAVAILABLE"
|
SERVICE_UNAVAILABLE = "SERVICE_UNAVAILABLE"
|
||||||
TIMEOUT = "TIMEOUT"
|
TIMEOUT = "TIMEOUT"
|
||||||
|
|
|
||||||
|
|
@ -198,3 +198,27 @@ class DocumentCreate(SQLModel):
|
||||||
file_path: str | None = None
|
file_path: str | None = None
|
||||||
file_size: int | None = None
|
file_size: int | None = None
|
||||||
file_type: str | None = None
|
file_type: str | None = None
|
||||||
|
|
||||||
|
|
||||||
|
class ApiKey(SQLModel, table=True):
|
||||||
|
"""
|
||||||
|
[AC-AISVC-50] API Key entity for lightweight authentication.
|
||||||
|
Keys are loaded into memory on startup for fast validation.
|
||||||
|
"""
|
||||||
|
|
||||||
|
__tablename__ = "api_keys"
|
||||||
|
|
||||||
|
id: uuid.UUID = Field(default_factory=uuid.uuid4, primary_key=True)
|
||||||
|
key: str = Field(..., description="API Key (unique)", unique=True, index=True)
|
||||||
|
name: str = Field(..., description="Key name/description for identification")
|
||||||
|
is_active: bool = Field(default=True, description="Whether the key is active")
|
||||||
|
created_at: datetime = Field(default_factory=datetime.utcnow, description="Creation time")
|
||||||
|
updated_at: datetime = Field(default_factory=datetime.utcnow, description="Last update time")
|
||||||
|
|
||||||
|
|
||||||
|
class ApiKeyCreate(SQLModel):
|
||||||
|
"""Schema for creating a new API key."""
|
||||||
|
|
||||||
|
key: str
|
||||||
|
name: str
|
||||||
|
is_active: bool = True
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,249 @@
|
||||||
|
"""
|
||||||
|
API Key management service.
|
||||||
|
[AC-AISVC-50] Lightweight authentication with in-memory cache.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import logging
|
||||||
|
import secrets
|
||||||
|
from datetime import datetime
|
||||||
|
from typing import Optional
|
||||||
|
|
||||||
|
from sqlalchemy import select
|
||||||
|
from sqlalchemy.ext.asyncio import AsyncSession
|
||||||
|
|
||||||
|
from app.models.entities import ApiKey, ApiKeyCreate
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
class ApiKeyService:
|
||||||
|
"""
|
||||||
|
[AC-AISVC-50] API Key management service.
|
||||||
|
|
||||||
|
Features:
|
||||||
|
- In-memory cache for fast validation
|
||||||
|
- Database persistence
|
||||||
|
- Hot-reload support
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self):
|
||||||
|
self._keys_cache: set[str] = set()
|
||||||
|
self._initialized: bool = False
|
||||||
|
|
||||||
|
async def initialize(self, session: AsyncSession) -> None:
|
||||||
|
"""
|
||||||
|
Load all active API keys from database into memory.
|
||||||
|
Should be called on application startup.
|
||||||
|
"""
|
||||||
|
result = await session.execute(
|
||||||
|
select(ApiKey).where(ApiKey.is_active == True)
|
||||||
|
)
|
||||||
|
keys = result.scalars().all()
|
||||||
|
|
||||||
|
self._keys_cache = {key.key for key in keys}
|
||||||
|
self._initialized = True
|
||||||
|
|
||||||
|
logger.info(f"[AC-AISVC-50] Loaded {len(self._keys_cache)} API keys into memory")
|
||||||
|
|
||||||
|
def validate_key(self, key: str) -> bool:
|
||||||
|
"""
|
||||||
|
Validate an API key against the in-memory cache.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
key: The API key to validate
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
True if the key is valid, False otherwise
|
||||||
|
"""
|
||||||
|
if not self._initialized:
|
||||||
|
logger.warning("[AC-AISVC-50] API key service not initialized")
|
||||||
|
return False
|
||||||
|
|
||||||
|
return key in self._keys_cache
|
||||||
|
|
||||||
|
def generate_key(self) -> str:
|
||||||
|
"""
|
||||||
|
Generate a new secure API key.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
A URL-safe random string
|
||||||
|
"""
|
||||||
|
return secrets.token_urlsafe(32)
|
||||||
|
|
||||||
|
async def create_key(
|
||||||
|
self,
|
||||||
|
session: AsyncSession,
|
||||||
|
key_create: ApiKeyCreate
|
||||||
|
) -> ApiKey:
|
||||||
|
"""
|
||||||
|
Create a new API key.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
session: Database session
|
||||||
|
key_create: Key creation data
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
The created ApiKey entity
|
||||||
|
"""
|
||||||
|
api_key = ApiKey(
|
||||||
|
key=key_create.key,
|
||||||
|
name=key_create.name,
|
||||||
|
is_active=key_create.is_active,
|
||||||
|
)
|
||||||
|
|
||||||
|
session.add(api_key)
|
||||||
|
await session.commit()
|
||||||
|
await session.refresh(api_key)
|
||||||
|
|
||||||
|
if api_key.is_active:
|
||||||
|
self._keys_cache.add(api_key.key)
|
||||||
|
|
||||||
|
logger.info(f"[AC-AISVC-50] Created API key: {api_key.name}")
|
||||||
|
return api_key
|
||||||
|
|
||||||
|
async def create_default_key(self, session: AsyncSession) -> Optional[ApiKey]:
|
||||||
|
"""
|
||||||
|
Create a default API key if none exists.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
The created ApiKey or None if keys already exist
|
||||||
|
"""
|
||||||
|
result = await session.execute(select(ApiKey).limit(1))
|
||||||
|
existing = result.scalar_one_or_none()
|
||||||
|
|
||||||
|
if existing:
|
||||||
|
return None
|
||||||
|
|
||||||
|
default_key = secrets.token_urlsafe(32)
|
||||||
|
api_key = ApiKey(
|
||||||
|
key=default_key,
|
||||||
|
name="Default API Key",
|
||||||
|
is_active=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
session.add(api_key)
|
||||||
|
await session.commit()
|
||||||
|
await session.refresh(api_key)
|
||||||
|
|
||||||
|
self._keys_cache.add(api_key.key)
|
||||||
|
|
||||||
|
logger.info(f"[AC-AISVC-50] Created default API key: {api_key.key}")
|
||||||
|
return api_key
|
||||||
|
|
||||||
|
async def delete_key(
|
||||||
|
self,
|
||||||
|
session: AsyncSession,
|
||||||
|
key_id: str
|
||||||
|
) -> bool:
|
||||||
|
"""
|
||||||
|
Delete an API key.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
session: Database session
|
||||||
|
key_id: The key ID to delete
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
True if deleted, False if not found
|
||||||
|
"""
|
||||||
|
import uuid
|
||||||
|
|
||||||
|
try:
|
||||||
|
key_uuid = uuid.UUID(key_id)
|
||||||
|
except ValueError:
|
||||||
|
return False
|
||||||
|
|
||||||
|
result = await session.execute(
|
||||||
|
select(ApiKey).where(ApiKey.id == key_uuid)
|
||||||
|
)
|
||||||
|
api_key = result.scalar_one_or_none()
|
||||||
|
|
||||||
|
if not api_key:
|
||||||
|
return False
|
||||||
|
|
||||||
|
key_value = api_key.key
|
||||||
|
await session.delete(api_key)
|
||||||
|
await session.commit()
|
||||||
|
|
||||||
|
self._keys_cache.discard(key_value)
|
||||||
|
|
||||||
|
logger.info(f"[AC-AISVC-50] Deleted API key: {api_key.name}")
|
||||||
|
return True
|
||||||
|
|
||||||
|
async def toggle_key(
|
||||||
|
self,
|
||||||
|
session: AsyncSession,
|
||||||
|
key_id: str,
|
||||||
|
is_active: bool
|
||||||
|
) -> Optional[ApiKey]:
|
||||||
|
"""
|
||||||
|
Toggle API key active status.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
session: Database session
|
||||||
|
key_id: The key ID to toggle
|
||||||
|
is_active: New active status
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
The updated ApiKey or None if not found
|
||||||
|
"""
|
||||||
|
import uuid
|
||||||
|
|
||||||
|
try:
|
||||||
|
key_uuid = uuid.UUID(key_id)
|
||||||
|
except ValueError:
|
||||||
|
return None
|
||||||
|
|
||||||
|
result = await session.execute(
|
||||||
|
select(ApiKey).where(ApiKey.id == key_uuid)
|
||||||
|
)
|
||||||
|
api_key = result.scalar_one_or_none()
|
||||||
|
|
||||||
|
if not api_key:
|
||||||
|
return None
|
||||||
|
|
||||||
|
api_key.is_active = is_active
|
||||||
|
api_key.updated_at = datetime.utcnow()
|
||||||
|
|
||||||
|
session.add(api_key)
|
||||||
|
await session.commit()
|
||||||
|
await session.refresh(api_key)
|
||||||
|
|
||||||
|
if is_active:
|
||||||
|
self._keys_cache.add(api_key.key)
|
||||||
|
else:
|
||||||
|
self._keys_cache.discard(api_key.key)
|
||||||
|
|
||||||
|
logger.info(f"[AC-AISVC-50] Toggled API key {api_key.name}: active={is_active}")
|
||||||
|
return api_key
|
||||||
|
|
||||||
|
async def list_keys(self, session: AsyncSession) -> list[ApiKey]:
|
||||||
|
"""
|
||||||
|
List all API keys.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
session: Database session
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
List of all ApiKey entities
|
||||||
|
"""
|
||||||
|
result = await session.execute(select(ApiKey))
|
||||||
|
return list(result.scalars().all())
|
||||||
|
|
||||||
|
async def reload_cache(self, session: AsyncSession) -> None:
|
||||||
|
"""
|
||||||
|
Reload all API keys from database into memory.
|
||||||
|
"""
|
||||||
|
self._keys_cache.clear()
|
||||||
|
await self.initialize(session)
|
||||||
|
logger.info("[AC-AISVC-50] API key cache reloaded")
|
||||||
|
|
||||||
|
|
||||||
|
_api_key_service: ApiKeyService | None = None
|
||||||
|
|
||||||
|
|
||||||
|
def get_api_key_service() -> ApiKeyService:
|
||||||
|
"""Get the global API key service instance."""
|
||||||
|
global _api_key_service
|
||||||
|
if _api_key_service is None:
|
||||||
|
_api_key_service = ApiKeyService()
|
||||||
|
return _api_key_service
|
||||||
|
|
@ -74,6 +74,18 @@ CREATE TABLE IF NOT EXISTS index_jobs (
|
||||||
updated_at TIMESTAMP WITHOUT TIME ZONE NOT NULL
|
updated_at TIMESTAMP WITHOUT TIME ZONE NOT NULL
|
||||||
);
|
);
|
||||||
|
|
||||||
|
-- ============================================
|
||||||
|
-- API Keys Table [AC-AISVC-50]
|
||||||
|
-- ============================================
|
||||||
|
CREATE TABLE IF NOT EXISTS api_keys (
|
||||||
|
id UUID NOT NULL PRIMARY KEY,
|
||||||
|
key VARCHAR NOT NULL UNIQUE,
|
||||||
|
name VARCHAR NOT NULL,
|
||||||
|
is_active BOOLEAN NOT NULL DEFAULT TRUE,
|
||||||
|
created_at TIMESTAMP WITHOUT TIME ZONE NOT NULL,
|
||||||
|
updated_at TIMESTAMP WITHOUT TIME ZONE NOT NULL
|
||||||
|
);
|
||||||
|
|
||||||
-- ============================================
|
-- ============================================
|
||||||
-- Indexes
|
-- Indexes
|
||||||
-- ============================================
|
-- ============================================
|
||||||
|
|
@ -100,6 +112,10 @@ CREATE INDEX IF NOT EXISTS ix_index_jobs_tenant_id ON index_jobs (tenant_id);
|
||||||
CREATE INDEX IF NOT EXISTS ix_index_jobs_tenant_doc ON index_jobs (tenant_id, doc_id);
|
CREATE INDEX IF NOT EXISTS ix_index_jobs_tenant_doc ON index_jobs (tenant_id, doc_id);
|
||||||
CREATE INDEX IF NOT EXISTS ix_index_jobs_tenant_status ON index_jobs (tenant_id, status);
|
CREATE INDEX IF NOT EXISTS ix_index_jobs_tenant_status ON index_jobs (tenant_id, status);
|
||||||
|
|
||||||
|
-- API Keys Indexes [AC-AISVC-50]
|
||||||
|
CREATE INDEX IF NOT EXISTS ix_api_keys_key ON api_keys (key);
|
||||||
|
CREATE INDEX IF NOT EXISTS ix_api_keys_is_active ON api_keys (is_active);
|
||||||
|
|
||||||
-- ============================================
|
-- ============================================
|
||||||
-- Verification
|
-- Verification
|
||||||
-- ============================================
|
-- ============================================
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue