Phase 5.14: API documentation - OpenAPI tags, response models, examples, docs

This commit is contained in:
Agent Zero
2026-07-23 22:44:54 +02:00
parent 3c1b2f227b
commit 66b6c32ed8
11 changed files with 716 additions and 28 deletions
+57 -1
View File
@@ -212,7 +212,63 @@ async def lifespan(app: FastAPI):
def create_app() -> FastAPI:
"""Create and configure the FastAPI application."""
settings = get_settings()
app = FastAPI(title="LeoCRM", version="1.0.0", lifespan=lifespan)
app = FastAPI(
title="LeoCRM",
version="1.0.0",
lifespan=lifespan,
description=(
"LeoCRM — Self-hosted CRM system for small sales teams.\n\n"
"## Authentication\n"
"All endpoints (except `/api/v1/auth/login` and `/api/v1/health`) require "
"a valid session cookie. Obtain a session by calling `POST /api/v1/auth/login`.\n\n"
"## Multi-Tenancy\n"
"All data is tenant-scoped. The tenant context is set automatically from the "
"authenticated session.\n\n"
"## Plugins\n"
"LeoCRM uses a plugin architecture. Core routes are always available. "
"Plugin routes (DMS, Mail, Calendar, Automation, etc.) are registered when "
"the corresponding plugin is active."
),
openapi_tags=[
{"name": "health", "description": "Health check endpoints — no auth required."},
{"name": "metrics", "description": "Prometheus metrics endpoint."},
{"name": "auth", "description": "Authentication: login, logout, session, password reset."},
{"name": "users", "description": "User management: CRUD, user-tenant assignments."},
{"name": "roles", "description": "Role management and permission definitions."},
{"name": "groups", "description": "User groups for contact assignment and filtering."},
{"name": "tenants", "description": "Tenant management and tenant switching."},
{"name": "notifications", "description": "User notifications and notification preferences."},
{"name": "contacts", "description": "Contact CRUD, contact persons, FTS search, export, soft-delete."},
{"name": "contact-folders", "description": "Contact folder management for organization."},
{"name": "entity-history", "description": "Audit trail and entity change history."},
{"name": "import-export", "description": "Bulk import and export of contacts and data."},
{"name": "plugins", "description": "Plugin management: list, install, activate, deactivate."},
{"name": "ai-copilot", "description": "AI copilot: chat, suggestions, conversation history."},
{"name": "workflows", "description": "Workflow definitions, instances, and execution."},
{"name": "user-preferences", "description": "Per-user preference settings."},
{"name": "currencies", "description": "Currency management for multi-currency support."},
{"name": "taxes", "description": "Tax rate management (VAT, sales tax)."},
{"name": "sequences", "description": "Number sequence management for invoices, quotes, etc."},
{"name": "system-settings", "description": "Tenant-level system settings (company info, theme)."},
{"name": "attachments", "description": "File attachments for contacts, companies, and entities."},
{"name": "addresses", "description": "Address management for contacts and companies."},
{"name": "audit", "description": "Audit log queries and compliance reporting."},
{"name": "automation", "description": "Automation engine: agents, automations, cron jobs, execution logs."},
{"name": "agents", "description": "AI agent definitions and agent runner endpoints."},
{"name": "dms", "description": "Document Management System: files, folders, sources, sharing."},
{"name": "mail", "description": "Email integration: IMAP accounts, folders, messages, send."},
{"name": "calendar", "description": "Calendar management: appointments, resources, recurrence."},
{"name": "search", "description": "Unified search across contacts, documents, emails, etc."},
{"name": "reports", "description": "Report generator: templates, rendering, scheduled reports."},
{"name": "entity-links", "description": "Entity linking: connect contacts to DMS documents and other entities."},
{"name": "kommunikation", "description": "Unified messaging: conversations, participants, messages."},
{"name": "ai-proactive", "description": "Proactive AI: insights, alerts, and recommendations."},
{"name": "ai-assistant", "description": "AI assistant: chat completions, tool calling, context awareness."},
{"name": "tags", "description": "Tag management: create, assign, search tags across entities."},
{"name": "permissions", "description": "Permission management: roles, field-level permissions, sharing."},
{"name": "public-share", "description": "Public sharing endpoints — no auth required, token-based access."},
],
)
app.add_middleware(
CORSMiddleware,
+1 -1
View File
@@ -26,7 +26,7 @@ from app.plugins.builtins.ai_ui_control.schemas import (
logger = logging.getLogger(__name__)
router = APIRouter()
router = APIRouter(tags=["ai-ui-control"])
# ─── REST endpoints (for AI agents) ───
+5 -3
View File
@@ -12,7 +12,9 @@ from app.core.auth import get_redis
from app.core.db import get_db
from app.core.rate_limit import check_rate_limit, get_client_ip, reset_rate_limit
from app.schemas.auth import (
AuthResponse,
LoginRequest,
MessageResponse,
PasswordResetConfirm,
PasswordResetRequest,
SwitchTenantRequest,
@@ -24,7 +26,7 @@ router = APIRouter(prefix="/api/v1/auth", tags=["auth"])
settings = get_settings()
@router.post("/login")
@router.post("/login", response_model=AuthResponse)
async def login(
request: Request,
body: LoginRequest,
@@ -91,7 +93,7 @@ async def login(
return resp
@router.post("/logout")
@router.post("/logout", response_model=MessageResponse)
async def logout(
request: Request,
db: AsyncSession = Depends(get_db),
@@ -112,7 +114,7 @@ async def logout(
return resp
@router.get("/me")
@router.get("/me", response_model=AuthResponse)
async def me(
request: Request,
db: AsyncSession = Depends(get_db),
+2 -1
View File
@@ -5,11 +5,12 @@ from __future__ import annotations
from fastapi import APIRouter
from app.core.monitoring import get_health_status
from app.schemas.common import HealthResponse
router = APIRouter(tags=["health"])
@router.get("/api/v1/health")
@router.get("/api/v1/health", response_model=HealthResponse)
async def health():
"""Health check — no auth required.
+2 -2
View File
@@ -19,7 +19,7 @@ from app.models.notification import (
NotificationPreference,
NotificationType,
)
from app.schemas.common import NotificationPreferenceUpdate
from app.schemas.common import NotificationPreferenceUpdate, UnreadCountResponse
router = APIRouter(prefix="/api/v1/notifications", tags=["notifications"])
@@ -67,7 +67,7 @@ async def mark_notification_read_endpoint(
}
@router.get("/unread-count")
@router.get("/unread-count", response_model=UnreadCountResponse)
async def unread_count_endpoint(
db: AsyncSession = Depends(get_db),
current_user: dict = Depends(require_permission("notifications:read")),
+3 -3
View File
@@ -9,13 +9,13 @@ from sqlalchemy.ext.asyncio import AsyncSession
from app.core.db import get_db
from app.deps import require_permission
from app.schemas.system_settings import SystemSettingsUpsert
from app.schemas.system_settings import SystemSettingsUpsert, SystemSettingsResponse
from app.services import system_settings_service
router = APIRouter(prefix="/api/v1/system-settings", tags=["system-settings"])
@router.get("")
@router.get("", response_model=SystemSettingsResponse)
async def get_system_settings(
db: AsyncSession = Depends(get_db),
current_user: dict = Depends(require_permission("settings:read")),
@@ -28,7 +28,7 @@ async def get_system_settings(
return result
@router.put("")
@router.put("", response_model=SystemSettingsResponse)
async def upsert_system_settings(
body: SystemSettingsUpsert,
db: AsyncSession = Depends(get_db),
+4 -4
View File
@@ -14,7 +14,7 @@ from app.core.db import get_db
from app.core.notifications import create_notification
from app.core.permissions import invalidate_permission_cache
from app.deps import require_permission
from app.schemas.user import UserCreate, UserUpdate
from app.schemas.user import UserCreate, UserUpdate, UserResponse, PaginatedUsers
from app.services.user_service import user_service, _UNSET
router = APIRouter(prefix="/api/v1/users", tags=["users"])
@@ -36,7 +36,7 @@ def _parse_role_id(raw: str | None) -> uuid.UUID | None:
) from None
@router.get("")
@router.get("", response_model=PaginatedUsers)
async def list_users(
page: int = Query(1, ge=1),
page_size: int = Query(25, ge=1, le=100),
@@ -49,7 +49,7 @@ async def list_users(
return await user_service.list_users(db, tenant_id, page, page_size, search)
@router.post("", status_code=status.HTTP_201_CREATED)
@router.post("", status_code=status.HTTP_201_CREATED, response_model=UserResponse)
async def create_user(
body: UserCreate,
db: AsyncSession = Depends(get_db),
@@ -103,7 +103,7 @@ async def create_user(
}
@router.get("/{user_id}")
@router.get("/{user_id}", response_model=UserResponse)
async def get_user(
user_id: str,
db: AsyncSession = Depends(get_db),
+8 -8
View File
@@ -6,8 +6,8 @@ from pydantic import BaseModel, EmailStr, Field
class LoginRequest(BaseModel):
email: EmailStr
password: str = Field(..., min_length=1)
email: EmailStr = Field(..., examples=["admin@leocrm.local"])
password: str = Field(..., min_length=1, examples=["secure-password"])
class PasswordResetRequest(BaseModel):
@@ -24,12 +24,12 @@ class SwitchTenantRequest(BaseModel):
class AuthResponse(BaseModel):
user_id: str
email: str
name: str
role: str
tenant_id: str
tenant_name: str | None = None
user_id: str = Field(..., examples=["550e8400-e29b-41d4-a716-446655440000"])
email: str = Field(..., examples=["admin@leocrm.local"])
name: str = Field(..., examples=["Admin User"])
role: str = Field(..., examples=["admin"])
tenant_id: str = Field(..., examples=["550e8400-e29b-41d4-a716-446655440001"])
tenant_name: str | None = Field(None, examples=["Acme GmbH"])
class MessageResponse(BaseModel):
+5 -5
View File
@@ -6,11 +6,11 @@ from pydantic import BaseModel, EmailStr, Field
class UserCreate(BaseModel):
email: EmailStr
name: str = Field(..., min_length=1, max_length=200)
password: str = Field(..., min_length=8)
role: str = Field(default="viewer")
role_id: str | None = Field(default=None, description="UUID of a custom Role")
email: EmailStr = Field(..., examples=["user@leocrm.local"])
name: str = Field(..., min_length=1, max_length=200, examples=["John Doe"])
password: str = Field(..., min_length=8, examples=["secure-password"])
role: str = Field(default="viewer", examples=["viewer"])
role_id: str | None = Field(default=None, description="UUID of a custom Role", examples=["550e8400-e29b-41d4-a716-446655440000"])
is_active: bool = True