From 66b6c32ed873d9d8751926cea0e48ddb52d9c4cb Mon Sep 17 00:00:00 2001 From: Agent Zero Date: Thu, 23 Jul 2026 22:44:54 +0200 Subject: [PATCH] Phase 5.14: API documentation - OpenAPI tags, response models, examples, docs --- app/main.py | 58 ++- app/plugins/builtins/ai_ui_control/routes.py | 2 +- app/routes/auth.py | 8 +- app/routes/health.py | 3 +- app/routes/notifications.py | 4 +- app/routes/system_settings.py | 6 +- app/routes/users.py | 8 +- app/schemas/auth.py | 16 +- app/schemas/user.py | 10 +- docs/api-documentation.md | 500 +++++++++++++++++++ tests/test_api_documentation.py | 129 +++++ 11 files changed, 716 insertions(+), 28 deletions(-) create mode 100644 docs/api-documentation.md create mode 100644 tests/test_api_documentation.py diff --git a/app/main.py b/app/main.py index 5f6364a..ead5400 100644 --- a/app/main.py +++ b/app/main.py @@ -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, diff --git a/app/plugins/builtins/ai_ui_control/routes.py b/app/plugins/builtins/ai_ui_control/routes.py index 1fcdfb3..a76921a 100644 --- a/app/plugins/builtins/ai_ui_control/routes.py +++ b/app/plugins/builtins/ai_ui_control/routes.py @@ -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) ─── diff --git a/app/routes/auth.py b/app/routes/auth.py index 7c868d0..9a640b9 100644 --- a/app/routes/auth.py +++ b/app/routes/auth.py @@ -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), diff --git a/app/routes/health.py b/app/routes/health.py index 5a8bcce..0fcde3b 100644 --- a/app/routes/health.py +++ b/app/routes/health.py @@ -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. diff --git a/app/routes/notifications.py b/app/routes/notifications.py index 8863be6..94a0a59 100644 --- a/app/routes/notifications.py +++ b/app/routes/notifications.py @@ -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")), diff --git a/app/routes/system_settings.py b/app/routes/system_settings.py index 356248b..9420e8e 100644 --- a/app/routes/system_settings.py +++ b/app/routes/system_settings.py @@ -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), diff --git a/app/routes/users.py b/app/routes/users.py index be24b2c..403138e 100644 --- a/app/routes/users.py +++ b/app/routes/users.py @@ -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), diff --git a/app/schemas/auth.py b/app/schemas/auth.py index 46224ed..c5a9f27 100644 --- a/app/schemas/auth.py +++ b/app/schemas/auth.py @@ -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): diff --git a/app/schemas/user.py b/app/schemas/user.py index 55faed6..0a811c0 100644 --- a/app/schemas/user.py +++ b/app/schemas/user.py @@ -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 diff --git a/docs/api-documentation.md b/docs/api-documentation.md new file mode 100644 index 0000000..59d0130 --- /dev/null +++ b/docs/api-documentation.md @@ -0,0 +1,500 @@ +# LeoCRM API Documentation + +> Auto-generated from FastAPI route enumeration. **295 endpoints** across **30 tag groups**. + +## Overview + +LeoCRM exposes a RESTful API under `/api/v1/`. All endpoints (except `/api/v1/health` and `/api/v1/auth/login`) require authentication via session cookie. + +- **Swagger UI**: `/docs` +- **ReDoc**: `/redoc` +- **OpenAPI JSON**: `/openapi.json` + +### Authentication + +1. Call `POST /api/v1/auth/login` with `{email, password}` → receives session cookie. +2. Include the session cookie in all subsequent requests. +3. Call `POST /api/v1/auth/logout` to invalidate the session. + +### Multi-Tenancy + +All data is tenant-scoped. The tenant context is derived from the authenticated session. Users with multiple tenants can switch via `POST /api/v1/auth/switch-tenant`. + +### Error Format + +```json +{"detail": "Error message", "code": "error_code", "fields": {"field": "error"}} +``` + +### Pagination + +List endpoints use `page` (1-based) and `page_size` (1-100) query parameters. Responses include `total`, `page`, `page_size`, and `items`. + +--- + +## Core Routes + +### health (1 endpoint) + +| Method | Path | Description | +|--------|------|-------------| +| GET | `/api/v1/health` | Health check — no auth required. Returns status + DB/Redis/storage/worker checks. **Response model**: `HealthResponse` | + +### metrics (1 endpoint) + +| Method | Path | Description | +|--------|------|-------------| +| GET | `/api/v1/metrics` | Prometheus metrics endpoint. | + +### auth (7 endpoints) + +| Method | Path | Description | +|--------|------|-------------| +| POST | `/api/v1/auth/login` | Login with email+password. Sets session cookie. **Request**: `LoginRequest`, **Response**: `AuthResponse` | +| POST | `/api/v1/auth/logout` | Logout — invalidate session, clear cookie. **Response**: `MessageResponse` | +| GET | `/api/v1/auth/me` | Get current user + active tenant. **Response**: `AuthResponse` | +| GET | `/api/v1/auth/me/permissions` | Get resolved permissions for current user. | +| POST | `/api/v1/auth/switch-tenant` | Switch active tenant. **Request**: `SwitchTenantRequest` | +| POST | `/api/v1/auth/password-reset/request` | Request password reset email. **Request**: `PasswordResetRequest` | +| POST | `/api/v1/auth/password-reset/confirm` | Confirm password reset with token. **Request**: `PasswordResetConfirm` | + +### users (5 endpoints) + +| Method | Path | Description | +|--------|------|-------------| +| GET | `/api/v1/users` | List users with pagination. **Response**: `PaginatedUsers` | +| POST | `/api/v1/users` | Create a new user (admin only). **Request**: `UserCreate`, **Response**: `UserResponse` | +| GET | `/api/v1/users/{user_id}` | Get a single user. **Response**: `UserResponse` | +| PATCH | `/api/v1/users/{user_id}` | Update user fields. **Request**: `UserUpdate` | +| DELETE | `/api/v1/users/{user_id}` | Delete/deactivate a user. | + +### roles (5 endpoints) + +| Method | Path | Description | +|--------|------|-------------| +| GET | `/api/v1/roles` | List all roles. | +| POST | `/api/v1/roles` | Create a custom role. | +| GET | `/api/v1/roles/permissions` | Get all available permission definitions. | +| PATCH | `/api/v1/roles/{role_id}` | Update role permissions. | +| DELETE | `/api/v1/roles/{role_id}` | Delete a custom role. | + +### groups (9 endpoints) + +| Method | Path | Description | +|--------|------|-------------| +| GET | `/api/v1/groups` | List all groups. | +| POST | `/api/v1/groups` | Create a new group. | +| GET | `/api/v1/groups/{group_id}` | Get a single group. | +| PATCH | `/api/v1/groups/{group_id}` | Update group. | +| DELETE | `/api/v1/groups/{group_id}` | Delete group. | +| GET | `/api/v1/groups/{group_id}/members` | List group members. | +| POST | `/api/v1/groups/{group_id}/members` | Add member to group. | +| DELETE | `/api/v1/groups/{group_id}/members/{user_id}` | Remove member from group. | +| GET | `/api/v1/groups/user/{user_id}` | Get groups for a user. | + +### tenants (4 endpoints) + +| Method | Path | Description | +|--------|------|-------------| +| GET | `/api/v1/tenants` | List tenants for current user. | +| POST | `/api/v1/tenants` | Create a new tenant. | +| GET | `/api/v1/tenants/{tenant_id}/users` | List users in a tenant. | +| POST | `/api/v1/tenants/{tenant_id}/users` | Add user to tenant. | + +### notifications (6 endpoints) + +| Method | Path | Description | +|--------|------|-------------| +| GET | `/api/v1/notifications` | List notifications with pagination. | +| PATCH | `/api/v1/notifications/{notification_id}/read` | Mark notification as read. | +| GET | `/api/v1/notifications/unread-count` | Get unread count. **Response**: `UnreadCountResponse` | +| GET | `/api/v1/notifications/types` | List notification types. | +| GET | `/api/v1/notifications/preferences` | Get notification preferences. | +| PATCH | `/api/v1/notifications/preferences/{type_key}` | Update notification preference. | + +### contacts (10 endpoints) + +| Method | Path | Description | +|--------|------|-------------| +| GET | `/api/v1/contacts` | List contacts with pagination, FTS search, type/folder filter, sorting. | +| POST | `/api/v1/contacts` | Create a contact (company or person). | +| GET | `/api/v1/contacts/export` | Stream contacts as CSV. | +| GET | `/api/v1/contacts/{contact_id}` | Get a single contact. | +| PUT | `/api/v1/contacts/{contact_id}` | Update contact. | +| DELETE | `/api/v1/contacts/{contact_id}` | Soft-delete contact. | +| GET | `/api/v1/contacts/{contact_id}/persons` | List contact persons. | +| POST | `/api/v1/contacts/{contact_id}/persons` | Add contact person. | +| PUT | `/api/v1/contacts/{contact_id}/persons/{person_id}` | Update contact person. | +| DELETE | `/api/v1/contacts/{contact_id}/persons/{person_id}` | Delete contact person. | + +### contact-folders (6 endpoints) + +| Method | Path | Description | +|--------|------|-------------| +| GET | `/api/v1/contact-folders` | List contact folders. | +| POST | `/api/v1/contact-folders` | Create a folder. | +| PUT | `/api/v1/contact-folders/{folder_id}` | Update folder. | +| DELETE | `/api/v1/contact-folders/{folder_id}` | Delete folder. | +| PUT | `/api/v1/contact-folders/{folder_id}/reorder` | Reorder folder. | +| PUT | `/api/v1/contact-folders/contacts/{contact_id}/move` | Move contact to folder. | + +### entity-history (3 endpoints) + +| Method | Path | Description | +|--------|------|-------------| +| GET | `/api/v1/entity-history/{entity_type}/{entity_id}` | Get change history for an entity. | +| POST | `/api/v1/entity-history/undo/{entity_type}/{entity_id}` | Undo last change. | +| POST | `/api/v1/entity-history/restore` | Restore entity to a specific version. | + +### import-export (2 endpoints) + +| Method | Path | Description | +|--------|------|-------------| +| POST | `/api/v1/import` | Import data from file. | +| POST | `/api/v1/import/preview` | Preview import data. | + +### plugins (11 endpoints) + +| Method | Path | Description | +|--------|------|-------------| +| GET | `/api/v1/plugins` | List all plugins. | +| GET | `/api/v1/plugins/manifest` | Get plugin manifest schema. | +| GET | `/api/v1/plugins/active-manifests` | Get manifests of all active plugins. | +| POST | `/api/v1/plugins/install-url` | Install plugin from URL. | +| POST | `/api/v1/plugins/upload` | Upload and install plugin ZIP. | +| POST | `/api/v1/plugins/{name}/install` | Install a discovered plugin. | +| POST | `/api/v1/plugins/{name}/activate` | Activate a plugin. | +| POST | `/api/v1/plugins/{name}/deactivate` | Deactivate a plugin. | +| GET | `/api/v1/plugins/{name}/config` | Get plugin configuration. | +| PATCH | `/api/v1/plugins/{name}/config` | Update plugin configuration. | +| DELETE | `/api/v1/plugins/{name}` | Uninstall a plugin. | + +### ai-copilot (3 endpoints) + +| Method | Path | Description | +|--------|------|-------------| +| POST | `/api/v1/ai/copilot/query` | Ask the AI copilot a question. | +| POST | `/api/v1/ai/copilot/execute` | Execute an AI copilot action. | +| GET | `/api/v1/ai/copilot/history` | Get copilot conversation history. | + +### workflows (10 endpoints) + +| Method | Path | Description | +|--------|------|-------------| +| GET | `/api/v1/workflows` | List workflow definitions. | +| POST | `/api/v1/workflows` | Create a workflow. | +| GET | `/api/v1/workflows/{workflow_id}` | Get a workflow. | +| PATCH | `/api/v1/workflows/{workflow_id}` | Update a workflow. | +| DELETE | `/api/v1/workflows/{workflow_id}` | Delete a workflow. | +| POST | `/api/v1/workflows/{workflow_id}/instances` | Start a workflow instance. | +| GET | `/api/v1/workflows/instances` | List workflow instances. | +| GET | `/api/v1/workflows/instances/{instance_id}` | Get a workflow instance. | +| POST | `/api/v1/workflows/instances/{instance_id}/advance` | Advance workflow to next step. | +| POST | `/api/v1/workflows/instances/{instance_id}/cancel` | Cancel a workflow instance. | + +### user-preferences (4 endpoints) + +| Method | Path | Description | +|--------|------|-------------| +| GET | `/api/v1/user/preferences` | Get all user preferences. | +| GET | `/api/v1/user/preferences/{key}` | Get a specific preference. | +| PUT | `/api/v1/user/preferences/{key}` | Set a preference. | +| DELETE | `/api/v1/user/preferences/{key}` | Delete a preference. | + +### currencies (4 endpoints) + +| Method | Path | Description | +|--------|------|-------------| +| GET | `/api/v1/currencies` | List currencies. | +| POST | `/api/v1/currencies` | Create a currency. | +| PATCH | `/api/v1/currencies/{currency_id}` | Update a currency. | +| DELETE | `/api/v1/currencies/{currency_id}` | Delete a currency. | + +### taxes (4 endpoints) + +| Method | Path | Description | +|--------|------|-------------| +| GET | `/api/v1/taxes` | List tax rates. | +| POST | `/api/v1/taxes` | Create a tax rate. | +| PATCH | `/api/v1/taxes/{tax_id}` | Update a tax rate. | +| DELETE | `/api/v1/taxes/{tax_id}` | Delete a tax rate. | + +### sequences (4 endpoints) + +| Method | Path | Description | +|--------|------|-------------| +| GET | `/api/v1/sequences` | List number sequences. | +| POST | `/api/v1/sequences` | Create a sequence. | +| PATCH | `/api/v1/sequences/{sequence_id}` | Update a sequence. | +| DELETE | `/api/v1/sequences/{sequence_id}` | Delete a sequence. | + +### system-settings (2 endpoints) + +| Method | Path | Description | +|--------|------|-------------| +| GET | `/api/v1/system-settings` | Get system settings. **Response**: `SystemSettingsResponse` | +| PUT | `/api/v1/system-settings` | Upsert system settings. **Request**: `SystemSettingsUpsert`, **Response**: `SystemSettingsResponse` | + +### attachments (4 endpoints) + +| Method | Path | Description | +|--------|------|-------------| +| GET | `/api/v1/attachments` | List attachments. | +| POST | `/api/v1/attachments` | Upload an attachment. | +| GET | `/api/v1/attachments/{attachment_id}` | Download an attachment. | +| DELETE | `/api/v1/attachments/{attachment_id}` | Delete an attachment. | + +### addresses (4 endpoints) + +| Method | Path | Description | +|--------|------|-------------| +| GET | `/api/v1/addresses` | List addresses. | +| POST | `/api/v1/addresses` | Create an address. | +| PATCH | `/api/v1/addresses/{address_id}` | Update an address. | +| DELETE | `/api/v1/addresses/{address_id}` | Delete an address. | + +### audit (1 endpoint) + +| Method | Path | Description | +|--------|------|-------------| +| GET | `/api/v1/audit-log` | Query audit log entries. | + +--- + +## Plugin Routes + +### automation (Automation & Agents) + +Agent Builder, Automation Builder, Cron-Scheduler, Agent Runner. + +| Method | Path | Description | +|--------|------|-------------| +| GET/POST | `/api/v1/automation/*` | Automation definitions, triggers, execution logs. | + +### agents (AI Agents) + +| Method | Path | Description | +|--------|------|-------------| +| GET/POST/PUT/DELETE | `/api/v1/agents/*` | AI agent CRUD and runner endpoints. | + +### dms (Document Management System) + +19 endpoints for file and folder management. + +| Method | Path | Description | +|--------|------|-------------| +| GET | `/api/v1/dms/files` | List files. | +| POST | `/api/v1/dms/files/upload` | Upload a file. | +| GET | `/api/v1/dms/files/{file_id}` | Get file metadata. | +| GET | `/api/v1/dms/files/{file_id}/preview` | Preview a file. | +| POST | `/api/v1/dms/files/{file_id}/share` | Share a file. | +| GET/POST | `/api/v1/dms/folders` | Folder CRUD. | +| GET | `/api/v1/dms/search` | Search documents. | +| GET | `/api/v1/dms/shared-with-me` | Files shared with current user. | + +### mail (Email Integration) + +50 endpoints for IMAP/SMTP email management. + +| Method | Path | Description | +|--------|------|-------------| +| GET/POST | `/api/v1/mail/accounts` | Email account management. | +| GET/POST | `/api/v1/mail/folders` | Folder management. | +| GET | `/api/v1/mail/{mail_id}` | Get a specific email. | +| POST | `/api/v1/mail/send` | Send an email. | +| POST | `/api/v1/mail/drafts` | Save draft. | +| GET | `/api/v1/mail/search` | Search emails. | +| GET | `/api/v1/mail/threads` | List email threads. | +| POST | `/api/v1/mail/pgp/keys` | PGP key management. | +| POST | `/api/v1/mail/rules` | Mail filter rules. | +| POST | `/api/v1/mail/vacation` | Vacation responder. | + +### calendar (Calendar & Scheduling) + +21 endpoints for calendar management. + +| Method | Path | Description | +|--------|------|-------------| +| GET/POST | `/api/v1/calendar/entries` | Calendar entry CRUD. | +| GET/POST | `/api/v1/calendars` | Calendar CRUD. | +| GET | `/api/v1/calendar/kanban` | Kanban board view. | +| POST | `/api/v1/calendar/import` | Import ICS. | +| GET | `/api/v1/calendar/{calendar_id}/ics-feed` | ICS feed. | +| POST | `/api/v1/resources` | Resource booking. | + +### search (Unified Search) + +7 endpoints for cross-entity search. + +| Method | Path | Description | +|--------|------|-------------| +| POST | `/api/v1/search` | Unified search across all entities. | +| GET | `/api/v1/search/providers` | List search providers. | +| POST | `/api/v1/search/reindex` | Rebuild search index. | +| POST | `/api/v1/search/similar` | Find similar entities. | +| GET | `/api/v1/search/suggest` | Search suggestions. | +| GET | `/api/v1/search/stats` | Search index statistics. | + +### reports (Report Generator) + +8 endpoints for report templates and generation. + +| Method | Path | Description | +|--------|------|-------------| +| GET/POST | `/api/v1/reports/templates` | Report template CRUD. | +| POST | `/api/v1/reports/generate` | Generate a report. | +| GET | `/api/v1/reports/{report_id}` | Get report status. | +| GET | `/api/v1/reports/{report_id}/download` | Download generated report. | + +### entity-links (Entity Linking) + +4 endpoints for connecting entities. + +| Method | Path | Description | +|--------|------|-------------| +| GET | `/api/v1/contacts/{contact_id}/files` | Get files linked to a contact. | +| POST/DELETE | `/api/v1/dms/files/{file_id}/link` | Link/unlink a file to an entity. | +| GET | `/api/v1/dms/files/{file_id}/links` | Get all links for a file. | + +### kommunikation (Unified Messaging) + +23 endpoints for conversations and messages. + +| Method | Path | Description | +|--------|------|-------------| +| GET/POST | `/api/v1/comm/conversations` | Conversation CRUD. | +| GET/POST | `/api/v1/comm/conversations/{id}/messages` | Message list and send. | +| POST | `/api/v1/comm/conversations/{id}/participants` | Manage participants. | +| POST | `/api/v1/comm/messages/{id}/reactions` | Message reactions. | +| GET | `/api/v1/comm/miniapps` | List mini-apps. | + +### ai-proactive (Proactive AI) + +8 endpoints for AI insights and suggestions. + +| Method | Path | Description | +|--------|------|-------------| +| GET | `/api/v1/ai-proactive/suggestions` | Get AI suggestions. | +| GET | `/api/v1/ai-proactive/suggestions/stream` | SSE stream of suggestions. | +| POST | `/api/v1/ai-proactive/suggestions/{id}/act` | Act on a suggestion. | +| POST | `/api/v1/ai-proactive/suggestions/{id}/dismiss` | Dismiss a suggestion. | +| GET/PUT | `/api/v1/ai-proactive/settings` | Proactive AI settings. | + +### ai-assistant (AI Assistant) + +30 endpoints for AI chat sessions, providers, models, and tools. + +| Method | Path | Description | +|--------|------|-------------| +| GET/POST | `/api/v1/ai/sessions` | Chat session CRUD. | +| GET | `/api/v1/ai/sessions/{id}/messages` | List messages in session. | +| POST | `/api/v1/ai/sessions/{id}/stream` | Stream chat completion (SSE). | +| GET/POST | `/api/v1/ai/providers` | AI provider CRUD. | +| GET/POST | `/api/v1/ai/models` | AI model CRUD. | +| GET/POST | `/api/v1/ai/presets` | Preset CRUD. | +| GET | `/api/v1/ai/tools` | List available AI tools. | + +### ai-ui-control (AI UI Control) + +WebSocket and REST endpoints for AI-driven UI control. + +| Method | Path | Description | +|--------|------|-------------| +| WebSocket | `/ws/ai-ui-control` | Real-time UI command stream. | +| POST | `/api/v1/ai-ui-control/command` | Send UI command from AI agent. | + +### tags (Tag Management) + +8 endpoints for tag CRUD and assignment. + +| Method | Path | Description | +|--------|------|-------------| +| GET/POST | `/api/v1/tags` | Tag CRUD. | +| POST/DELETE | `/api/v1/tags/assign` | Assign/remove tag from entity. | +| POST | `/api/v1/tags/bulk-assign` | Bulk assign tags. | +| GET | `/api/v1/tags/{tag_id}/entities` | Get entities with a tag. | + +### permissions (Permission Management) + +5 endpoints for file-level permissions and share links. + +| Method | Path | Description | +|--------|------|-------------| +| GET/POST | `/api/v1/dms/files/{file_id}/permissions` | File permission CRUD. | +| POST | `/api/v1/dms/files/{file_id}/share-link` | Create public share link. | +| DELETE | `/api/v1/dms/share-links/{link_id}` | Revoke share link. | + +### public-share (Public Sharing) + +2 endpoints — no auth required, token-based access. + +| Method | Path | Description | +|--------|------|-------------| +| GET | `/api/public/share/{token}` | Access shared resource via token. | +| POST | `/api/public/share/{token}` | Interact with shared resource. | + +--- + +## Schema Examples + +### LoginRequest +```json +{"email": "admin@leocrm.local", "password": "secure-password"} +``` + +### AuthResponse +```json +{ + "user_id": "550e8400-e29b-41d4-a716-446655440000", + "email": "admin@leocrm.local", + "name": "Admin User", + "role": "admin", + "tenant_id": "550e8400-e29b-41d4-a716-446655440001", + "tenant_name": "Acme GmbH" +} +``` + +### UserCreate +```json +{ + "email": "user@leocrm.local", + "name": "John Doe", + "password": "secure-password", + "role": "viewer", + "is_active": true +} +``` + +### UserResponse +```json +{ + "id": "550e8400-e29b-41d4-a716-446655440000", + "email": "user@leocrm.local", + "name": "John Doe", + "role": "viewer", + "is_active": true, + "tenant_id": "550e8400-e29b-41d4-a716-446655440001" +} +``` + +### HealthResponse +```json +{"status": "healthy", "version": "1.0.0"} +``` + +### UnreadCountResponse +```json +{"count": 5} +``` + +--- + +## KI Integration Notes + +### For AI Agents + +1. **Health Check**: Run `python scripts/ai_health_check.py --base-url --token ` before and after updates. +2. **Deploy**: Use `python scripts/ai_deploy.py --dry-run` to preview, then `python scripts/ai_deploy.py` for real deployment. +3. **OpenAPI**: The full OpenAPI spec is available at `/openapi.json` — use it for dynamic endpoint discovery. +4. **Safe Methods**: GET endpoints are safe to probe. POST/PUT/DELETE require careful payload construction. +5. **Session Auth**: AI agents should call `POST /api/v1/auth/login` first, then use the returned cookie for all subsequent requests. diff --git a/tests/test_api_documentation.py b/tests/test_api_documentation.py new file mode 100644 index 0000000..149a130 --- /dev/null +++ b/tests/test_api_documentation.py @@ -0,0 +1,129 @@ +"""Tests for OpenAPI documentation completeness (Task 5.14).""" + +from __future__ import annotations + +import os +import sys + +import pytest + +sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) + + +def test_openapi_tags_configured(): + """FastAPI app should have openapi_tags configured with descriptions.""" + from app.main import app + + assert app.openapi_tags is not None + assert len(app.openapi_tags) > 0 + tag_names = {t["name"] for t in app.openapi_tags} + # Core tags should be present + assert "health" in tag_names + assert "auth" in tag_names + assert "users" in tag_names + assert "contacts" in tag_names + assert "system-settings" in tag_names + + +def test_openapi_tags_have_descriptions(): + """Each openapi_tag should have a description.""" + from app.main import app + + for tag in app.openapi_tags: + assert "description" in tag + assert len(tag["description"]) > 0 + + +def test_app_has_description(): + """FastAPI app should have a description for OpenAPI docs.""" + from app.main import app + + assert app.description is not None + assert len(app.description) > 0 + assert "LeoCRM" in app.description or "CRM" in app.description + + +def test_all_api_routes_have_tags(): + """All API routes should have at least one tag for OpenAPI grouping.""" + from app.main import app + + untagged = [] + for route in app.routes: + if not hasattr(route, "methods") or not hasattr(route, "path"): + continue + if not route.path.startswith("/api/"): + continue + methods = route.methods - {"HEAD", "OPTIONS"} if route.methods else set() + if not methods: + continue + tags = getattr(route, "tags", []) or [] + if not tags: + untagged.append(f"{route.path} [{','.join(sorted(methods))}]") + assert len(untagged) == 0, f"Routes without tags: {untagged}" + + +def test_key_routes_have_response_model(): + """Key routes should have response_model set for OpenAPI schema generation.""" + from app.main import app + + # Check that at least some routes have response_model + routes_with_response_model = 0 + for route in app.routes: + if not hasattr(route, "methods") or not hasattr(route, "path"): + continue + if not route.path.startswith("/api/"): + continue + response_model = getattr(route, "response_model", None) + if response_model is not None: + routes_with_response_model += 1 + assert routes_with_response_model >= 5, ( + f"Expected at least 5 routes with response_model, got {routes_with_response_model}" + ) + + +def test_auth_login_has_auth_response_model(): + """POST /api/v1/auth/login should have AuthResponse as response_model.""" + from app.main import app + from app.schemas.auth import AuthResponse + + for route in app.routes: + if ( + hasattr(route, "path") + and route.path == "/api/v1/auth/login" + and hasattr(route, "response_model") + ): + assert route.response_model == AuthResponse + return + pytest.fail("Route /api/v1/auth/login not found") + + +def test_health_has_health_response_model(): + """GET /api/v1/health should have HealthResponse as response_model.""" + from app.main import app + from app.schemas.common import HealthResponse + + for route in app.routes: + if ( + hasattr(route, "path") + and route.path == "/api/v1/health" + and hasattr(route, "response_model") + ): + assert route.response_model == HealthResponse + return + pytest.fail("Route /api/v1/health not found") + + +def test_api_documentation_md_exists(): + """docs/api-documentation.md should exist and have content.""" + doc_path = os.path.join( + os.path.dirname(os.path.dirname(os.path.abspath(__file__))), + "docs", + "api-documentation.md", + ) + assert os.path.isfile(doc_path), "docs/api-documentation.md not found" + with open(doc_path) as f: + content = f.read() + assert "LeoCRM API Documentation" in content + assert "auth" in content.lower() + assert "contacts" in content.lower() + assert "plugins" in content.lower()