diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 0a7dd2e..cf0eaaa 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -77,6 +77,7 @@ jobs: enable-cache: true - run: uv sync --frozen - run: uv run python manage.py check + - run: uv run python manage.py spectacular --file /tmp/asp-openapi.yaml - run: uv run python manage.py test frontend: diff --git a/asp-doc b/asp-doc index cf44b69..0868441 160000 --- a/asp-doc +++ b/asp-doc @@ -1 +1 @@ -Subproject commit cf44b69e415d5341c7ed11eefea802491c13c29e +Subproject commit 08684417048a39304c9bf9e0f9c4701aefe46a0e diff --git a/backend/apps/common/apps.py b/backend/apps/common/apps.py index df04969..1d52b2e 100644 --- a/backend/apps/common/apps.py +++ b/backend/apps/common/apps.py @@ -4,3 +4,6 @@ from django.apps import AppConfig class CommonConfig(AppConfig): default_auto_field = "django.db.models.BigAutoField" name = "apps.common" + + def ready(self): + from . import openapi # noqa: F401 diff --git a/backend/apps/common/openapi.py b/backend/apps/common/openapi.py new file mode 100644 index 0000000..3fe386c --- /dev/null +++ b/backend/apps/common/openapi.py @@ -0,0 +1,100 @@ +from drf_spectacular.extensions import OpenApiAuthenticationExtension +from drf_spectacular.openapi import AutoSchema +from drf_spectacular.plumbing import build_serializer_context +from drf_spectacular.types import OpenApiTypes +from rest_framework.generics import GenericAPIView +from rest_framework.views import APIView + + +class AspAutoSchema(AutoSchema): + def _get_serializer(self): + view = self.view + context = build_serializer_context(view) + try: + if isinstance(view, GenericAPIView): + if view.__class__.get_serializer == GenericAPIView.get_serializer: + return view.get_serializer_class()(context=context) + return view.get_serializer(context=context) + if isinstance(view, APIView): + if callable(getattr(view, "get_serializer", None)): + return view.get_serializer(context=context) + if callable(getattr(view, "get_serializer_class", None)): + return view.get_serializer_class()(context=context) + if hasattr(view, "serializer_class"): + return view.serializer_class + except Exception: + return None + return None + + def get_request_serializer(self): + serializer = self._get_serializer() + if serializer is None and self.method in ("POST", "PUT", "PATCH"): + return OpenApiTypes.OBJECT + return serializer + + def get_response_serializers(self): + return self._get_serializer() or OpenApiTypes.OBJECT + + +class BearerAuthenticationScheme(OpenApiAuthenticationExtension): + target_class = "rest_framework_simplejwt.authentication.JWTAuthentication" + name = "bearerAuth" + priority = 1 + + def get_security_definition(self, auto_schema): + return { + "type": "http", + "scheme": "bearer", + "bearerFormat": "JWT", + "description": "Use the format: Bearer ", + } + + +class ApiKeyAuthenticationScheme(OpenApiAuthenticationExtension): + target_class = "apps.accounts.authentication.ApiKeyAuthentication" + name = "apiKeyAuth" + + def get_security_definition(self, auto_schema): + return { + "type": "apiKey", + "in": "header", + "name": "Authorization", + "description": "Use the format: Api-Key ", + } + + +BUSINESS_TAG_PREFIXES = ( + ("/api/agent/v1/", "Agent API"), + ("/api/auth/api-keys", "API Keys"), + ("/api/auth/users", "Users"), + ("/api/auth/", "Auth"), + ("/api/alerts", "Alerts"), + ("/api/artifacts", "Artifacts"), + ("/api/attachments", "Attachments"), + ("/api/audit-logs", "Audit"), + ("/api/cases", "Cases"), + ("/api/comments", "Comments"), + ("/api/custom/", "Custom"), + ("/api/dashboard", "Dashboard"), + ("/api/enrichments", "Enrichments"), + ("/api/health", "System"), + ("/api/inbox", "Inbox"), + ("/api/knowledge", "Knowledge"), + ("/api/metadata", "Metadata"), + ("/api/playbooks", "Playbooks"), + ("/api/saved-table-filters", "Preferences"), + ("/api/settings", "Settings"), + ("/api/user-table-preferences", "Preferences"), + ("/api/webhook", "Webhooks"), +) + + +def postprocess_business_tags(result, generator, request, public): + for path, methods in result.get("paths", {}).items(): + tag = next((candidate for prefix, candidate in BUSINESS_TAG_PREFIXES if path.startswith(prefix)), None) + if tag is None: + continue + for operation in methods.values(): + if isinstance(operation, dict): + operation["tags"] = [tag] + return result diff --git a/backend/asp/asgi.py b/backend/asp/asgi.py index a16afad..a19ec27 100644 --- a/backend/asp/asgi.py +++ b/backend/asp/asgi.py @@ -4,7 +4,9 @@ ASGI config for asp project. import os +from django.conf import settings from django.core.asgi import get_asgi_application +from django.contrib.staticfiles.handlers import ASGIStaticFilesHandler from channels.routing import ProtocolTypeRouter, URLRouter from apps.common.logging import configure_process_file_logging @@ -22,3 +24,6 @@ application = ProtocolTypeRouter({ "http": django_application, "websocket": JWTAuthMiddleware(URLRouter(websocket_urlpatterns)), }) + +if settings.DEBUG: + application = ASGIStaticFilesHandler(application) diff --git a/backend/asp/settings.py b/backend/asp/settings.py index aed2839..4bef906 100644 --- a/backend/asp/settings.py +++ b/backend/asp/settings.py @@ -46,6 +46,8 @@ INSTALLED_APPS = [ # Third party "rest_framework", "rest_framework_simplejwt", + "drf_spectacular", + "drf_spectacular_sidecar", "corsheaders", "django_filters", "storages", @@ -164,9 +166,52 @@ REST_FRAMEWORK = { "rest_framework.filters.SearchFilter", "rest_framework.filters.OrderingFilter", ], + "DEFAULT_SCHEMA_CLASS": "apps.common.openapi.AspAutoSchema", "EXCEPTION_HANDLER": "apps.common.exceptions.custom_exception_handler", } +SPECTACULAR_SETTINGS = { + "TITLE": "Agentic SOC Platform API", + "DESCRIPTION": "HTTP API for Agentic SOC Platform. External automation integrations should prefer API keys.", + "VERSION": "0.5.0", + "SERVE_INCLUDE_SCHEMA": False, + "COMPONENT_SPLIT_REQUEST": True, + "SWAGGER_UI_DIST": "SIDECAR", + "SWAGGER_UI_FAVICON_HREF": "SIDECAR", + "REDOC_DIST": "SIDECAR", + "SWAGGER_UI_SETTINGS": { + "deepLinking": True, + "persistAuthorization": True, + }, + "POSTPROCESSING_HOOKS": [ + "drf_spectacular.hooks.postprocess_schema_enums", + "apps.common.openapi.postprocess_business_tags", + ], + "TAGS": [ + {"name": "Auth", "description": "Login, refresh token, profile, and current user operations."}, + {"name": "Users", "description": "User administration APIs."}, + {"name": "API Keys", "description": "Personal API key management APIs."}, + {"name": "Cases", "description": "Case investigation records."}, + {"name": "Alerts", "description": "Security alert records."}, + {"name": "Artifacts", "description": "Indicators, assets, and other related artifacts."}, + {"name": "Enrichments", "description": "Enrichment records and creation APIs."}, + {"name": "Playbooks", "description": "Playbook records and execution APIs."}, + {"name": "Knowledge", "description": "Knowledge base records."}, + {"name": "Comments", "description": "Record comment APIs."}, + {"name": "Attachments", "description": "Attachment upload, metadata, and download APIs."}, + {"name": "Audit", "description": "Audit log query APIs."}, + {"name": "Inbox", "description": "User inbox message APIs."}, + {"name": "Preferences", "description": "User table preference APIs."}, + {"name": "Settings", "description": "System configuration APIs."}, + {"name": "Custom", "description": "Custom module, playbook, and SIEM definition APIs."}, + {"name": "Dashboard", "description": "Dashboard summary APIs."}, + {"name": "Metadata", "description": "Resource metadata APIs."}, + {"name": "Webhooks", "description": "Inbound alert webhook APIs."}, + {"name": "Agent API", "description": "Versioned APIs for agent and CLI integrations."}, + {"name": "System", "description": "System health and utility APIs."}, + ], +} + SIMPLE_JWT = { "ACCESS_TOKEN_LIFETIME": timedelta(hours=8), "REFRESH_TOKEN_LIFETIME": timedelta(days=7), diff --git a/backend/asp/urls.py b/backend/asp/urls.py index 87c66af..4ed36ab 100644 --- a/backend/asp/urls.py +++ b/backend/asp/urls.py @@ -1,6 +1,10 @@ from django.urls import path, include +from drf_spectacular.views import SpectacularAPIView, SpectacularRedocView, SpectacularSwaggerView urlpatterns = [ + path("api/schema/", SpectacularAPIView.as_view(), name="schema"), + path("api/docs/", SpectacularSwaggerView.as_view(url_name="schema"), name="swagger-ui"), + path("api/redoc/", SpectacularRedocView.as_view(url_name="schema"), name="redoc"), path("api/", include("apps.accounts.urls")), path("api/", include("apps.settings.urls")), path("api/", include("apps.common.urls")), diff --git a/backend/pyproject.toml b/backend/pyproject.toml index 6097257..7237273 100644 --- a/backend/pyproject.toml +++ b/backend/pyproject.toml @@ -27,6 +27,8 @@ dependencies = [ "gunicorn==26.0.0", "uvicorn==0.49.0", "pycti>=7.260701.0", + "drf-spectacular>=0.30.0", + "drf-spectacular-sidecar>=2026.7.1", ] [[tool.uv.index]] diff --git a/backend/readme.md b/backend/readme.md index 75896da..38ce35d 100644 --- a/backend/readme.md +++ b/backend/readme.md @@ -34,3 +34,9 @@ Product, deployment, and operations documentation lives in the docs site: - https://asp.viperrtp.com/asp/quick-start/deployment/ - https://asp.viperrtp.com/asp/quick-start/first-login/ - https://asp.viperrtp.com/asp/settings/users/ + +Runtime API documentation is served by the backend: + +- Swagger UI: `/api/docs/` +- Redoc: `/api/redoc/` +- OpenAPI schema: `/api/schema/` diff --git a/backend/uv.lock b/backend/uv.lock index 4f17c15..9a547db 100644 --- a/backend/uv.lock +++ b/backend/uv.lock @@ -65,6 +65,8 @@ dependencies = [ { name = "djangorestframework" }, { name = "djangorestframework-simplejwt" }, { name = "djangorestframework-stubs" }, + { name = "drf-spectacular" }, + { name = "drf-spectacular-sidecar" }, { name = "elasticsearch" }, { name = "gunicorn" }, { name = "httpx" }, @@ -93,6 +95,8 @@ requires-dist = [ { name = "djangorestframework", specifier = "==3.17.1" }, { name = "djangorestframework-simplejwt", specifier = "==5.5.1" }, { name = "djangorestframework-stubs", specifier = "==3.17.0" }, + { name = "drf-spectacular", specifier = ">=0.30.0" }, + { name = "drf-spectacular-sidecar", specifier = ">=2026.7.1" }, { name = "elasticsearch", specifier = "==9.4.1" }, { name = "gunicorn", specifier = "==26.0.0" }, { name = "httpx", specifier = "==0.28.1" }, @@ -109,6 +113,15 @@ requires-dist = [ { name = "uvicorn", specifier = "==0.49.0" }, ] +[[package]] +name = "attrs" +version = "26.1.0" +source = { registry = "https://mirrors.aliyun.com/pypi/simple/" } +sdist = { url = "https://mirrors.aliyun.com/pypi/packages/9a/8e/82a0fe20a541c03148528be8cac2408564a6c9a0cc7e9171802bc1d26985/attrs-26.1.0.tar.gz", hash = "sha256:d03ceb89cb322a8fd706d4fb91940737b6642aa36998fe130a9bc96c985eff32" } +wheels = [ + { url = "https://mirrors.aliyun.com/pypi/packages/64/b4/17d4b0b2a2dc85a6df63d1157e028ed19f90d4cd97c36717afef2bc2f395/attrs-26.1.0-py3-none-any.whl", hash = "sha256:c647aa4a12dfbad9333ca4e71fe62ddc36f4e63b2d260a37a8b83d2f043ac309" }, +] + [[package]] name = "boto3" version = "1.43.36" @@ -499,6 +512,35 @@ wheels = [ { url = "https://mirrors.aliyun.com/pypi/packages/72/ff/6e27d4aea12f67d14e0c0ec41869307ae98ad3816858ddc2a56bc3f4e0b5/djangorestframework_stubs-3.17.0-py3-none-any.whl", hash = "sha256:babe2703f0401507780848439f49f76222a178b4fc73a6dcb30d0952a0a6dbc6" }, ] +[[package]] +name = "drf-spectacular" +version = "0.30.0" +source = { registry = "https://mirrors.aliyun.com/pypi/simple/" } +dependencies = [ + { name = "django" }, + { name = "djangorestframework" }, + { name = "inflection" }, + { name = "jsonschema" }, + { name = "pyyaml" }, + { name = "uritemplate" }, +] +sdist = { url = "https://mirrors.aliyun.com/pypi/packages/50/43/41d25039a6a53545420ebc98eb9f877ec9fe30c7bd03fefabcaf9b953af7/drf_spectacular-0.30.0.tar.gz", hash = "sha256:53e79e7ba00e240441b63c32273754a5368e4c2ab44a19f2595277cc1cd559c9" } +wheels = [ + { url = "https://mirrors.aliyun.com/pypi/packages/c3/56/74dd7b45bbde6d24494220b98d6961cb1200b63a1800332b430daa2c4551/drf_spectacular-0.30.0-py3-none-any.whl", hash = "sha256:006cf5921ebe20a9bd24f7c846261ebbf78780be5961b0d6e87afaa82afd62ff" }, +] + +[[package]] +name = "drf-spectacular-sidecar" +version = "2026.7.1" +source = { registry = "https://mirrors.aliyun.com/pypi/simple/" } +dependencies = [ + { name = "django" }, +] +sdist = { url = "https://mirrors.aliyun.com/pypi/packages/7a/51/9e038d14bf51a0bd051e8bcb690287349c908fa7ba69021d9f3e5d5ac51f/drf_spectacular_sidecar-2026.7.1.tar.gz", hash = "sha256:40113c4066c7bc3ef15a7ce1c40cda227a907a9986748024a813a9e0595eba25" } +wheels = [ + { url = "https://mirrors.aliyun.com/pypi/packages/9d/76/d08f5c79f7643dbff4512605c28b75481966ed6c8cc9b397c9dd2ee91cd1/drf_spectacular_sidecar-2026.7.1-py3-none-any.whl", hash = "sha256:bc6d50c9b64660e45e09296d39553b3e759eedd825fc41d631ee5b3f88e0c5de" }, +] + [[package]] name = "elastic-transport" version = "9.4.2" @@ -650,6 +692,15 @@ wheels = [ { url = "https://mirrors.aliyun.com/pypi/packages/fa/5e/f8e9a1d23b9c20a551a8a02ea3637b4642e22c2626e3a13a9a29cdea99eb/importlib_metadata-8.7.1-py3-none-any.whl", hash = "sha256:5a1f80bf1daa489495071efbb095d75a634cf28a8bc299581244063b53176151" }, ] +[[package]] +name = "inflection" +version = "0.5.1" +source = { registry = "https://mirrors.aliyun.com/pypi/simple/" } +sdist = { url = "https://mirrors.aliyun.com/pypi/packages/e1/7e/691d061b7329bc8d54edbf0ec22fbfb2afe61facb681f9aaa9bff7a27d04/inflection-0.5.1.tar.gz", hash = "sha256:1a29730d366e996aaacffb2f1f1cb9593dc38e2ddd30c91250c6dde09ea9b417" } +wheels = [ + { url = "https://mirrors.aliyun.com/pypi/packages/59/91/aa6bde563e0085a02a435aa99b49ef75b0a4b062635e606dab23ce18d720/inflection-0.5.1-py2.py3-none-any.whl", hash = "sha256:f38b2b640938a4f35ade69ac3d053042959b62a0f1076a5bbaa1b9526605a8a2" }, +] + [[package]] name = "jiter" version = "0.15.0" @@ -715,6 +766,33 @@ wheels = [ { url = "https://mirrors.aliyun.com/pypi/packages/9e/6a/a83720e953b1682d2d109d3c2dbb0bc9bf28cc1cbc205be4ef4be5da709d/jsonpointer-3.1.1-py3-none-any.whl", hash = "sha256:8ff8b95779d071ba472cf5bc913028df06031797532f08a7d5b602d8b2a488ca" }, ] +[[package]] +name = "jsonschema" +version = "4.26.0" +source = { registry = "https://mirrors.aliyun.com/pypi/simple/" } +dependencies = [ + { name = "attrs" }, + { name = "jsonschema-specifications" }, + { name = "referencing" }, + { name = "rpds-py" }, +] +sdist = { url = "https://mirrors.aliyun.com/pypi/packages/b3/fc/e067678238fa451312d4c62bf6e6cf5ec56375422aee02f9cb5f909b3047/jsonschema-4.26.0.tar.gz", hash = "sha256:0c26707e2efad8aa1bfc5b7ce170f3fccc2e4918ff85989ba9ffa9facb2be326" } +wheels = [ + { url = "https://mirrors.aliyun.com/pypi/packages/69/90/f63fb5873511e014207a475e2bb4e8b2e570d655b00ac19a9a0ca0a385ee/jsonschema-4.26.0-py3-none-any.whl", hash = "sha256:d489f15263b8d200f8387e64b4c3a75f06629559fb73deb8fdfb525f2dab50ce" }, +] + +[[package]] +name = "jsonschema-specifications" +version = "2025.9.1" +source = { registry = "https://mirrors.aliyun.com/pypi/simple/" } +dependencies = [ + { name = "referencing" }, +] +sdist = { url = "https://mirrors.aliyun.com/pypi/packages/19/74/a633ee74eb36c44aa6d1095e7cc5569bebf04342ee146178e2d36600708b/jsonschema_specifications-2025.9.1.tar.gz", hash = "sha256:b540987f239e745613c7a9176f3edb72b832a4ac465cf02712288397832b5e8d" } +wheels = [ + { url = "https://mirrors.aliyun.com/pypi/packages/41/45/1a4ed80516f02155c51f51e8cedb3c1902296743db0bbc66608a0db2814f/jsonschema_specifications-2025.9.1-py3-none-any.whl", hash = "sha256:98802fee3a11ee76ecaca44429fda8a41bff98b00a0f2838151b113f210cc6fe" }, +] + [[package]] name = "langchain-core" version = "1.4.8" @@ -1167,6 +1245,19 @@ wheels = [ { url = "https://mirrors.aliyun.com/pypi/packages/fd/0a/c2345ebf1ebe70840ce3f6c6ee612f8fa749cfbd1b03069c53bf0c62aaad/redis-8.0.1-py3-none-any.whl", hash = "sha256:47daa35a058c23468d6437f17a8c76882cb316b838ef763036af99b96cedd743" }, ] +[[package]] +name = "referencing" +version = "0.37.0" +source = { registry = "https://mirrors.aliyun.com/pypi/simple/" } +dependencies = [ + { name = "attrs" }, + { name = "rpds-py" }, +] +sdist = { url = "https://mirrors.aliyun.com/pypi/packages/22/f5/df4e9027acead3ecc63e50fe1e36aca1523e1719559c499951bb4b53188f/referencing-0.37.0.tar.gz", hash = "sha256:44aefc3142c5b842538163acb373e24cce6632bd54bdb01b21ad5863489f50d8" } +wheels = [ + { url = "https://mirrors.aliyun.com/pypi/packages/2c/58/ca301544e1fa93ed4f80d724bf5b194f6e4b945841c5bfd555878eea9fcb/referencing-0.37.0-py3-none-any.whl", hash = "sha256:381329a9f99628c9069361716891d34ad94af76e461dcb0335825aecc7692231" }, +] + [[package]] name = "regex" version = "2026.5.9" @@ -1234,6 +1325,72 @@ wheels = [ { url = "https://mirrors.aliyun.com/pypi/packages/3f/51/d4db610ef29373b879047326cbf6fa98b6c1969d6f6dc423279de2b1be2c/requests_toolbelt-1.0.0-py2.py3-none-any.whl", hash = "sha256:cccfdd665f0a24fcf4726e690f65639d272bb0637b9b92dfd91a5568ccf6bd06" }, ] +[[package]] +name = "rpds-py" +version = "2026.6.3" +source = { registry = "https://mirrors.aliyun.com/pypi/simple/" } +sdist = { url = "https://mirrors.aliyun.com/pypi/packages/aa/2a/9618a122aeb2a169a28b03889a2995fe297588964333d4a7d67bdf46e147/rpds_py-2026.6.3.tar.gz", hash = "sha256:1cebd1337c242e4ec2293e541f712b2da849b29f48f0c293684b71c0632625d4" } +wheels = [ + { url = "https://mirrors.aliyun.com/pypi/packages/b6/36/7fbe9dcdaf857fb3f63c2a2284b62492d95f5e8334e947e5fb6e7f68c9be/rpds_py-2026.6.3-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:931908d9fc855d8f74783377822be318edb6dcb19e47169dc038f9a1bf60b06e" }, + { url = "https://mirrors.aliyun.com/pypi/packages/ba/54/f785cc3d3f60839ca57a5af4927a9f347b07b2799c373fc20f7949f87c7e/rpds_py-2026.6.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:d7469697dce35be237db177d42e2a2ee26e6dcc5fc052078a6fefabd288c6edd" }, + { url = "https://mirrors.aliyun.com/pypi/packages/63/ef/d4cdaf309e6b095b43597103cf8c0b951d6cca2acce68c474f75ec12e0c7/rpds_py-2026.6.3-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bcfbcf66006befb9fd2aeaa9e01feaf881b4dc330a02ba07d2322b1c11be7b5d" }, + { url = "https://mirrors.aliyun.com/pypi/packages/96/4a/9559a68b7ee15db09d7981212e8c2e219d2a1d6d4faa0391d813c3496a36/rpds_py-2026.6.3-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:847927daf4cffbd4e90e42bc890069897101edd015f956cb8721b3473372edda" }, + { url = "https://mirrors.aliyun.com/pypi/packages/ef/75/8964aa7d2c6e8ac43eba8eb6e6b0fdda1f46d39f2fc3e6aa9f2cb17f485d/rpds_py-2026.6.3-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:aca6c1ef08a82bfe327cc156da694660f599923e2e6665b6d81c9c2d0ac9ffc8" }, + { url = "https://mirrors.aliyun.com/pypi/packages/8f/97/6908094ac804115e65aedfd90f1b5fee4eebebd3f6c4cfc5419939267565/rpds_py-2026.6.3-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:ae50181a047c871561212bb97f7932a2d45fb53e947bd9b57ebad85b529cbc53" }, + { url = "https://mirrors.aliyun.com/pypi/packages/d1/9c/0d1fdc2e7aba23e290d603bc494e97bd205bae262ce33c6b32a69768ed5e/rpds_py-2026.6.3-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dc319e5a1de4b6913aac94bf6a2f9e847371e0a140a43dd4991db1a09bc2d504" }, + { url = "https://mirrors.aliyun.com/pypi/packages/c4/fe/f0209ca4a9ed074bc8acb44dfd0e81c3122e94c9689f5645b7973a866719/rpds_py-2026.6.3-cp314-cp314-manylinux_2_31_riscv64.whl", hash = "sha256:e4316bf32babbed84e691e352faf967ce2f0f024174a8643c37c94a1080374fc" }, + { url = "https://mirrors.aliyun.com/pypi/packages/c6/8d/f1cc54c616b9d8897de8738aac148d20afca93f68187475fe194d09a71b9/rpds_py-2026.6.3-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:8c6e5a2f750cc71c3e3b11d71661f21d6f9bc6cebc6564b1466417a1ec03ec77" }, + { url = "https://mirrors.aliyun.com/pypi/packages/fb/04/aafff00f73aeca2945f734f1d483c64ab8f472d0864ab02377fd8e89c3b2/rpds_py-2026.6.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:4470ce197d4090875cf6affbf1f853338387428df97c4fb7b7106317b8214698" }, + { url = "https://mirrors.aliyun.com/pypi/packages/fd/cc/e229663b9e4ddac5a4acbe9085dd80a71af2a5d356b8b39d6bff233f24b0/rpds_py-2026.6.3-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:ea964164cc9afa72d4d9b23cc28dafae93693c0a53e0b42acbff15b22c3f9ddd" }, + { url = "https://mirrors.aliyun.com/pypi/packages/e3/7a/8a0e6d3e6cd066af108b71b43122c3fe158dd9eb86acac626593a2582eb1/rpds_py-2026.6.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:639c8929aa0afe81be836b04de888460d6bed38b9c54cfc18da8f6bfabf5af5d" }, + { url = "https://mirrors.aliyun.com/pypi/packages/87/03/2a69ab618a789cf6cf85c86bb844c62d090e700ab1a2aa676b3741b6c516/rpds_py-2026.6.3-cp314-cp314-win32.whl", hash = "sha256:882076c00c0a608b131187055ddc5ae29f2e7eaf870d6168980420d58528a5c8" }, + { url = "https://mirrors.aliyun.com/pypi/packages/85/62/a3892ba945f4e24c78f352e5de3c7620d8479f73f211406a97263d13c7d2/rpds_py-2026.6.3-cp314-cp314-win_amd64.whl", hash = "sha256:0be972be84cfcaf46c8c6edf690ca0f154ac17babf1f6a955a51579b34ad2dc5" }, + { url = "https://mirrors.aliyun.com/pypi/packages/3d/e7/c2bd44dc831931815ad11ebb5f430b5a0a4d3caa9de837107876c30c3432/rpds_py-2026.6.3-cp314-cp314-win_arm64.whl", hash = "sha256:2a9c6f195058cb45335e8cc3802745c603d716eb96bc9625950c1aac71c0c703" }, + { url = "https://mirrors.aliyun.com/pypi/packages/79/9c/fff7b74bce9a091ec9a012a03f9ff5f69364eaf9451060dfc4486da2ffdd/rpds_py-2026.6.3-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:f90938e92afda60266da758ee7d363447f7f0138c9559f9e1811629580582d90" }, + { url = "https://mirrors.aliyun.com/pypi/packages/e9/44/77bcb1168b33704908295533d27f10eb811e9e3e193e8993dc99572211d3/rpds_py-2026.6.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:ec829541c45bca16e61c7ae50c20501f213605beb75d1aba91a6ee37fbbb56a4" }, + { url = "https://mirrors.aliyun.com/pypi/packages/87/3c/7a9081c7c9e645b39efe19e4ffbeccd80add246327cd9b888aecffd72317/rpds_py-2026.6.3-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:afd70d95892096cdb26f15a00c45907b17817577aa8d1c76b2dcc2788391f9e9" }, + { url = "https://mirrors.aliyun.com/pypi/packages/f7/69/af47021eb7dad6ff3396cb001c08f0f3c4d06c20253f75be6421a59fe6b7/rpds_py-2026.6.3-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:29dfa0533a5d4c94d4dfa1b694fcb56c9c63aad8330ffdd816fd225d0a7a162f" }, + { url = "https://mirrors.aliyun.com/pypi/packages/81/fc/a3bcf517084396a6dd258c592567a3c011ba4557f2fde23dceaf26e74f2e/rpds_py-2026.6.3-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:af05d726809bff6b141be124d4c7ce998f9c9c7f30edb1f46c07aa103d540b41" }, + { url = "https://mirrors.aliyun.com/pypi/packages/c9/eb/13d529d1788135425c7bf207f8463458ca5d92e43f3f701365b83e9dffc1/rpds_py-2026.6.3-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9826217f048f620d9a712672818bf231442c1b35d96b227a07eabd11b4bb6945" }, + { url = "https://mirrors.aliyun.com/pypi/packages/8e/f4/b7ac49f30013aba8f7b9566b1dd07e81de95e708c1374b7bacc5b9bc5c9c/rpds_py-2026.6.3-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:536bceea4fa4acf7e1c61da2b5786304367c816c8895be71b8f537c480b0ea1f" }, + { url = "https://mirrors.aliyun.com/pypi/packages/31/86/6260bafa622f788b07ddec0e52d810305c8b9b0b8c27f58a2ab04bf62b4f/rpds_py-2026.6.3-cp314-cp314t-manylinux_2_31_riscv64.whl", hash = "sha256:bc0011654b91cc4fb2ae701bec0a0ba1e552c0714247fa7af6c59e0ccfa3a4e1" }, + { url = "https://mirrors.aliyun.com/pypi/packages/19/c3/03f1ee79a047b48daeca157c89a18509cde22b6b951d642b9b0af1be660a/rpds_py-2026.6.3-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:539d75de9e0d536c84ff18dfeb805398e58227001ce09231a26a08b9aed1ee0e" }, + { url = "https://mirrors.aliyun.com/pypi/packages/f0/95/8ed0cd8c377dca12aea498f119fe639fc474d1461545c39d2b5872eb1c0f/rpds_py-2026.6.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:166cf54d9f44fc6ceb53c7860258dde44a81406646de79f8ed3234fca3b6e538" }, + { url = "https://mirrors.aliyun.com/pypi/packages/d3/f2/0eb57f0eaa83f8fc152a7e03de968ab77e1f00732bebc892b190c6eebde7/rpds_py-2026.6.3-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:d34c20167764fbcf927194d532dd7e0c56772f0a5f943fa5ef9e9afbba8fb9db" }, + { url = "https://mirrors.aliyun.com/pypi/packages/5b/de/e0674bdbc3ef7634989b3f854c3f34bc1f587d36e5bfdc5c378d57034619/rpds_py-2026.6.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:ea7bb13b7c9a29791f87a0387ba7d3ad3a6d783d827e4d3f27b40a0ff44495e2" }, + { url = "https://mirrors.aliyun.com/pypi/packages/f2/f6/21101359743cd136ada781e8210a85769578422ba460672eea0e29739200/rpds_py-2026.6.3-cp314-cp314t-win32.whl", hash = "sha256:6de4744d05bd1aa1be4ed7ea1189e3979196808008113bbbf899a460966b925e" }, + { url = "https://mirrors.aliyun.com/pypi/packages/a6/b2/9574d4d44f7760c2aa32d92a0a4f41698e33f5b204a0bf5c9758f52c79d5/rpds_py-2026.6.3-cp314-cp314t-win_amd64.whl", hash = "sha256:c7b9a2f8f4d8e90af72571d3d495deebdd7e3c75451f5b41719aee166e940fc2" }, + { url = "https://mirrors.aliyun.com/pypi/packages/08/ae/f23a2697e6ee6340a578b0f136be6483657bef0c6f9497b752bb5c0964bb/rpds_py-2026.6.3-cp315-cp315-macosx_10_12_x86_64.whl", hash = "sha256:e059c5dde6452b44424bd1834557556c226b57781dee1227af23518459722b13" }, + { url = "https://mirrors.aliyun.com/pypi/packages/c3/63/e7b3a1a5358dd32c930a1062d8e15b67fd6e8922e81df9e91706d66ee5c8/rpds_py-2026.6.3-cp315-cp315-macosx_11_0_arm64.whl", hash = "sha256:2f7c26fbc5acd2522b95d4177fe4710ffd8e9b20529e703ffbf8db4d93903f05" }, + { url = "https://mirrors.aliyun.com/pypi/packages/ec/64/10a85681916ca55fffb91b0a211f84e34297c109243484dd6394660a8a7c/rpds_py-2026.6.3-cp315-cp315-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a3086b538543802f84c843911242db20447de00d8752dd0efc936dbcf02218ba" }, + { url = "https://mirrors.aliyun.com/pypi/packages/76/c2/baf95c7c38823e12ba34407c5f5767a89e5cf2233895e56f608167ae9493/rpds_py-2026.6.3-cp315-cp315-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:8f2e5c5ee828d42cb11760761c0af6507927bec42d0ad5458f97c9203b054617" }, + { url = "https://mirrors.aliyun.com/pypi/packages/6a/94/0aad06c72d65101e11d33528d438cda99a39ce0da99466e156158f2541d3/rpds_py-2026.6.3-cp315-cp315-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ed0c1e5d10cdc7135537988c74a0188da68e2f3c30813ba3744ab1e42e0480f9" }, + { url = "https://mirrors.aliyun.com/pypi/packages/b5/17/de3f5a479a1f056535d7489819639d8cd591ea6281d700390b43b1abd745/rpds_py-2026.6.3-cp315-cp315-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8c2642a7603ec0b16ed77da4555db3b4b472341904873788327c0b0d7b95f1bb" }, + { url = "https://mirrors.aliyun.com/pypi/packages/46/7d/bf09bd1b145bb2671c03e1e6d1ab8651858d90d8c7dfeadd85a37a934fd8/rpds_py-2026.6.3-cp315-cp315-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8e4320744c1ffdd95a603def63344bfab2d33edeab301c5007e7de9f9f5b3885" }, + { url = "https://mirrors.aliyun.com/pypi/packages/a3/ea/1bb734f314b8be319149ddee80b18bd41372bdcfbdf88d28131c0cd37719/rpds_py-2026.6.3-cp315-cp315-manylinux_2_31_riscv64.whl", hash = "sha256:a9f4645593036b81bbdb36b9c8e0ea0d1c3fee968c4d59db0344c14087ef143a" }, + { url = "https://mirrors.aliyun.com/pypi/packages/4b/93/d9611e5b25e26df9a3649813ed66193ace9347a7c7fc4ab7cf70e94851c0/rpds_py-2026.6.3-cp315-cp315-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:e55d236be29255554da47abe5c577637db7c24a02b8b46f0ca9524c855801868" }, + { url = "https://mirrors.aliyun.com/pypi/packages/c3/cb/99d77e16e5534ae1d90629bbe419ba6ee170833a6a85e3aa1cc41726fbbc/rpds_py-2026.6.3-cp315-cp315-musllinux_1_2_aarch64.whl", hash = "sha256:24e9c5386e16669b674a69c156c8eeefcb578f3b3397b713b08e6d60f3c7b187" }, + { url = "https://mirrors.aliyun.com/pypi/packages/59/15/11a29755f790cef7a2f755e8e14f4f0c33f39489e1893a632a2eee59672b/rpds_py-2026.6.3-cp315-cp315-musllinux_1_2_i686.whl", hash = "sha256:c60924535c75f1566b6eb75b5c31a48a43fef04fa2d0d201acbad8a9969c6107" }, + { url = "https://mirrors.aliyun.com/pypi/packages/68/86/0c27547e21644da938fb530f7e1a8148dd24d02db07e7a5f2567a17ce710/rpds_py-2026.6.3-cp315-cp315-musllinux_1_2_x86_64.whl", hash = "sha256:38a2fea2787428f811719ceb9114cb78964a3138838320c29ac39526c79c16ba" }, + { url = "https://mirrors.aliyun.com/pypi/packages/29/71/4d8fcf700931815594bce892255bbd973b94efaf0fc1932b0590df18d886/rpds_py-2026.6.3-cp315-cp315-win32.whl", hash = "sha256:d483fe17f01ad64b7bf7cc38fcefff1ca9fb83f8c2b2542b68f97ffe0611b369" }, + { url = "https://mirrors.aliyun.com/pypi/packages/eb/62/b577562de0edbb55b2be85ce5fd09c33e386b9b13eee09833af4240fd5c4/rpds_py-2026.6.3-cp315-cp315-win_amd64.whl", hash = "sha256:67e3a721ffc5d8d2210d3671872298c4a84e4b8035cfe42ffd7cde35d772b146" }, + { url = "https://mirrors.aliyun.com/pypi/packages/c8/95/d6d0b2509825141eef60669a5739eec88dbc6a48053d6c92993a5704defe/rpds_py-2026.6.3-cp315-cp315-win_arm64.whl", hash = "sha256:6e84adbcf4bf841aed8116a8264b9f50b4cb3e7bd89b516122e616ac56ca269e" }, + { url = "https://mirrors.aliyun.com/pypi/packages/b7/bf/f3ea278f0afd615c1d0f19cb69043a41526e2bb600c2b536eb192218eb27/rpds_py-2026.6.3-cp315-cp315t-macosx_10_12_x86_64.whl", hash = "sha256:ae6dd8f10bd17aad820876d24caec9efdafd80a318d16c0a48edb5e136902c6b" }, + { url = "https://mirrors.aliyun.com/pypi/packages/9d/29/9907bdf1c5346763cf10b7f6852aad86652168c259def904cbe0082c5864/rpds_py-2026.6.3-cp315-cp315t-macosx_11_0_arm64.whl", hash = "sha256:bdbd97738551fca3917c1bd7188bec1920bb520104f28e7e1007f9ceb17b7690" }, + { url = "https://mirrors.aliyun.com/pypi/packages/6f/2c/8e03767b5778ef25cebf74a7a91a2c3806f8eced4c92cb7406bbe060756d/rpds_py-2026.6.3-cp315-cp315t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8b95977e7211527ab0ba576e286d023389fbeeb32a6b7b771665d333c60e5342" }, + { url = "https://mirrors.aliyun.com/pypi/packages/2e/e1/df2a7e1ba2efd796af26194250b8d42c821b46592311595162af9ef0528d/rpds_py-2026.6.3-cp315-cp315t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:d15fde0e6fb0d88a60d221204873743e5d9f0b7d29165e62cd86d0413ad74ba6" }, + { url = "https://mirrors.aliyun.com/pypi/packages/6b/de/8a0814d1946af29cb068fb259aa8622f856df1d0bab58429448726b537f5/rpds_py-2026.6.3-cp315-cp315t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a136d453475ac0fcbda502ef1e6504bd28d6d904700915d278deeab0d00fe140" }, + { url = "https://mirrors.aliyun.com/pypi/packages/df/f3/f19e0c852ba13694f5a79f3b719331051573cb5693feacf8a88ffffc3a71/rpds_py-2026.6.3-cp315-cp315t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f826877d462181e5eb1c26a0026b8d0cab05d99844ecb6d8bf3627a2ca0c0442" }, + { url = "https://mirrors.aliyun.com/pypi/packages/e2/ae/7ec3a9d2d4351f99e37bcb06b6b6f954512646bfdbf9742e1de727865daf/rpds_py-2026.6.3-cp315-cp315t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:79486287de1730dbaff3dbd124d0ca4d2ef7f9d29bf2544f1f93c09b5bcbbd12" }, + { url = "https://mirrors.aliyun.com/pypi/packages/d3/ac/9cee911dff2aaa9a5a8354f6610bf2e6a616de9197c5fff4f54f82585f1e/rpds_py-2026.6.3-cp315-cp315t-manylinux_2_31_riscv64.whl", hash = "sha256:808345f53cb952433ca2816f1604ff3515608a81784954f38d4452acfe8e61d5" }, + { url = "https://mirrors.aliyun.com/pypi/packages/83/6b/7c2a07ba88d1e9a936612f7a5d067467ed03d971d5a06f7d309dff044a7e/rpds_py-2026.6.3-cp315-cp315t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:1967debc37f64f2c4dc90a7f563aec558b471966e12adcac4e1c4240496b6ebf" }, + { url = "https://mirrors.aliyun.com/pypi/packages/97/0b/776ffcb66783637b0031f6d58d6fb55913c8b5abf00aeecd46bf933fb477/rpds_py-2026.6.3-cp315-cp315t-musllinux_1_2_aarch64.whl", hash = "sha256:f0840b5b17057f7fd918b76183a4b5a0635f43e14eb2ce60dce1d4ee4707ea00" }, + { url = "https://mirrors.aliyun.com/pypi/packages/55/33/ba3bc04d7092bd553c9b2b195624992d2cc4f3de1f380b7b93cbee67bd79/rpds_py-2026.6.3-cp315-cp315t-musllinux_1_2_i686.whl", hash = "sha256:faa679d19a6696fd54259ad321251ad77a13e70e03dd834daa762a44fb6196ef" }, + { url = "https://mirrors.aliyun.com/pypi/packages/8b/71/14edf065f04630b1a8472f7653cad03f6c478bcf95ea0e6aed55451e33ea/rpds_py-2026.6.3-cp315-cp315t-musllinux_1_2_x86_64.whl", hash = "sha256:23a439f31ccbeff1574e24889128821d1f7917470e830cf6544dced1c662262a" }, + { url = "https://mirrors.aliyun.com/pypi/packages/ba/76/65002b08596c389105720a8c0d22298b8dc25a4baf89b2ce431343c8b1de/rpds_py-2026.6.3-cp315-cp315t-win32.whl", hash = "sha256:913ca42ccad3f8cc6e292b587ae8ae49c8c823e5dce51a736252fc7c7cdfa577" }, + { url = "https://mirrors.aliyun.com/pypi/packages/8c/97/d855d6b3c322d1f27e26f5241c42016b56cf01377ea8ed348285f54652f0/rpds_py-2026.6.3-cp315-cp315t-win_amd64.whl", hash = "sha256:ae3d4fe8c0b9213624fdce7279d70e3b148b682ca20719ebd193a23ebfa47324" }, +] + [[package]] name = "s3transfer" version = "0.19.0" @@ -1447,6 +1604,15 @@ wheels = [ { url = "https://mirrors.aliyun.com/pypi/packages/ce/e4/dccd7f47c4b64213ac01ef921a1337ee6e30e8c6466046018326977efd95/tzdata-2026.2-py2.py3-none-any.whl", hash = "sha256:bbe9af844f658da81a5f95019480da3a89415801f6cc966806612cc7169bffe7" }, ] +[[package]] +name = "uritemplate" +version = "4.2.0" +source = { registry = "https://mirrors.aliyun.com/pypi/simple/" } +sdist = { url = "https://mirrors.aliyun.com/pypi/packages/98/60/f174043244c5306c9988380d2cb10009f91563fc4b31293d27e17201af56/uritemplate-4.2.0.tar.gz", hash = "sha256:480c2ed180878955863323eea31b0ede668795de182617fef9c6ca09e6ec9d0e" } +wheels = [ + { url = "https://mirrors.aliyun.com/pypi/packages/a9/99/3ae339466c9183ea5b8ae87b34c0b897eda475d2aec2307cae60e5cd4f29/uritemplate-4.2.0-py3-none-any.whl", hash = "sha256:962201ba1c4edcab02e60f9a0d3821e82dfc5d2d6662a21abd533879bdb8a686" }, +] + [[package]] name = "urllib3" version = "2.7.0" diff --git a/docs/superpowers/plans/2026-07-22-api-documentation.md b/docs/superpowers/plans/2026-07-22-api-documentation.md new file mode 100644 index 0000000..9bc2ac1 --- /dev/null +++ b/docs/superpowers/plans/2026-07-22-api-documentation.md @@ -0,0 +1,331 @@ +# API Documentation Implementation Plan + +> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. + +**Goal:** Add maintainable Swagger/OpenAPI documentation for the full backend HTTP API and add lightweight documentation-site guidance for HTTP and realtime integration. + +**Architecture:** Use `drf-spectacular` to generate OpenAPI 3 schema from Django REST Framework views and serializers, served by the backend at `/api/schema/`, `/api/docs/`, and `/api/redoc/`. Use sidecar-packaged Swagger UI/Redoc assets for self-hosted deployments. Keep WebSocket protocol documentation in VitePress because OpenAPI does not describe websocket message flows. + +**Tech Stack:** Django, Django REST Framework, SimpleJWT, custom API key auth, drf-spectacular, drf-spectacular-sidecar, VitePress. + +--- + +### Task 1: Add OpenAPI dependencies and backend settings + +**Files:** +- Modify: `backend/pyproject.toml` +- Modify: `backend/asp/settings.py` + +- [ ] **Step 1: Add dependencies** + +Add these dependencies to `backend/pyproject.toml`: + +```toml +"drf-spectacular>=0.29.0", +"drf-spectacular-sidecar>=2026.1.1", +``` + +- [ ] **Step 2: Configure installed apps and DRF schema class** + +Update `backend/asp/settings.py`: + +```python +INSTALLED_APPS = [ + # Third party + "rest_framework", + "rest_framework_simplejwt", + "drf_spectacular", + "drf_spectacular_sidecar", + "corsheaders", +] + +REST_FRAMEWORK = { + "DEFAULT_SCHEMA_CLASS": "drf_spectacular.openapi.AutoSchema", +} +``` + +- [ ] **Step 3: Add OpenAPI settings** + +Add `SPECTACULAR_SETTINGS` in `backend/asp/settings.py`: + +```python +SPECTACULAR_SETTINGS = { + "TITLE": "Agentic SOC Platform API", + "DESCRIPTION": "HTTP API for Agentic SOC Platform. External integrations should prefer API keys for automation.", + "VERSION": "0.5.0", + "SERVE_INCLUDE_SCHEMA": False, + "SWAGGER_UI_DIST": "SIDECAR", + "SWAGGER_UI_FAVICON_HREF": "SIDECAR", + "REDOC_DIST": "SIDECAR", + "COMPONENT_SPLIT_REQUEST": True, + "SECURITY": [ + {"bearerAuth": []}, + {"apiKeyAuth": []}, + ], +} +``` + +- [ ] **Step 4: Sync dependencies** + +Run: + +```powershell +Set-Location -Path 'C:\Code\agentic-soc-platform\backend' +uv sync +``` + +Expected: dependencies resolve and install without errors. + +--- + +### Task 2: Add schema views and authentication extensions + +**Files:** +- Create: `backend/apps/common/openapi.py` +- Modify: `backend/asp/urls.py` + +- [ ] **Step 1: Define authentication extensions** + +Create `backend/apps/common/openapi.py`: + +```python +from drf_spectacular.extensions import OpenApiAuthenticationExtension + + +class ApiKeyAuthenticationScheme(OpenApiAuthenticationExtension): + target_class = "apps.accounts.authentication.ApiKeyAuthentication" + name = "apiKeyAuth" + + def get_security_definition(self, auto_schema): + return { + "type": "apiKey", + "in": "header", + "name": "Authorization", + "description": "Use the format: Api-Key ", + } +``` + +- [ ] **Step 2: Ensure extensions load** + +Import the module from `backend/apps/common/apps.py` inside `ready()`: + +```python +class CommonConfig(AppConfig): + default_auto_field = "django.db.models.BigAutoField" + name = "apps.common" + + def ready(self): + from . import openapi # noqa: F401 +``` + +- [ ] **Step 3: Register schema routes** + +Update `backend/asp/urls.py`: + +```python +from drf_spectacular.views import SpectacularAPIView, SpectacularRedocView, SpectacularSwaggerView + +urlpatterns = [ + path("api/schema/", SpectacularAPIView.as_view(), name="schema"), + path("api/docs/", SpectacularSwaggerView.as_view(url_name="schema"), name="swagger-ui"), + path("api/redoc/", SpectacularRedocView.as_view(url_name="schema"), name="redoc"), +] +``` + +- [ ] **Step 4: Validate schema route import** + +Run: + +```powershell +Set-Location -Path 'C:\Code\agentic-soc-platform\backend' +.\.venv\Scripts\python.exe manage.py check +``` + +Expected: `System check identified no issues`. + +--- + +### Task 3: Add schema tags and minimal APIView annotations + +**Files:** +- Modify: `backend/apps/common/openapi.py` +- Modify: `backend/apps/common/views.py` +- Modify: APIView-heavy modules as needed: `backend/apps/agent_api/views.py`, `backend/apps/settings/views.py`, `backend/apps/settings/custom_views.py`, `backend/apps/dashboard/views.py`, `backend/apps/webhook/views.py`, `backend/apps/attachments/views.py`, `backend/apps/preferences/views.py` + +- [ ] **Step 1: Add preprocessing hook for business tags** + +Add a hook in `backend/apps/common/openapi.py`: + +```python +def assign_business_tags(endpoints): + tag_map = ( + ("/api/auth/", "Auth"), + ("/api/auth/users", "Users"), + ("/api/auth/api-keys", "API Keys"), + ("/api/cases", "Cases"), + ("/api/alerts", "Alerts"), + ("/api/artifacts", "Artifacts"), + ("/api/comments", "Comments"), + ("/api/attachments", "Attachments"), + ("/api/settings", "Settings"), + ("/api/dashboard", "Dashboard"), + ("/api/agent/v1", "Agent API"), + ("/api/webhook", "Webhooks"), + ("/api/user-table-preferences", "Preferences"), + ("/api/saved-table-filters", "Preferences"), + ("/api/health", "System"), + ("/api/metadata", "Metadata"), + ) + for path, path_regex, method, callback in endpoints: + for prefix, tag in tag_map: + if path.startswith(prefix): + callback.cls._spectacular_annotation = getattr(callback.cls, "_spectacular_annotation", {}) + callback.cls._spectacular_annotation["tags"] = [tag] + break + return endpoints +``` + +- [ ] **Step 2: Wire preprocessing hook** + +Add to `SPECTACULAR_SETTINGS`: + +```python +"PREPROCESSING_HOOKS": [ + "apps.common.openapi.assign_business_tags", +], +``` + +- [ ] **Step 3: Annotate APIViews that lack serializers** + +For high-warning APIViews, use `extend_schema` with `OpenApiTypes.OBJECT` where exact schemas are not yet serializer-backed: + +```python +from drf_spectacular.utils import OpenApiResponse, extend_schema +from drf_spectacular.types import OpenApiTypes + + +@extend_schema( + responses={200: OpenApiResponse(response=OpenApiTypes.OBJECT)}, +) +def get(self, request): + ... +``` + +- [ ] **Step 4: Generate schema** + +Run: + +```powershell +Set-Location -Path 'C:\Code\agentic-soc-platform\backend' +.\.venv\Scripts\python.exe manage.py spectacular --file $env:TEMP\asp-openapi.yaml +``` + +Expected: command exits successfully. Warnings are acceptable in the first implementation phase. + +--- + +### Task 4: Add docs-site HTTP API and realtime guide + +**Files:** +- Create: `asp-doc/docs/zh/asp/integrations/api/index.md` +- Create: `asp-doc/docs/en/asp/integrations/api/index.md` +- Modify: `asp-doc/docs/.vitepress/config/zh.ts` +- Modify: `asp-doc/docs/.vitepress/config/en.ts` + +- [ ] **Step 1: Add Chinese guide** + +Create `asp-doc/docs/zh/asp/integrations/api/index.md`: + +```markdown +# API 集成 + +ASP 后端提供实时生成的 OpenAPI 文档,用于外部系统集成和调试。 + +## 文档入口 + +- Swagger UI: `/api/docs/` +- Redoc: `/api/redoc/` +- OpenAPI Schema: `/api/schema/` + +## 认证 + +自动化集成推荐使用 API Key: + +```http +Authorization: Api-Key +``` + +交互式用户也可以使用 JWT: + +```http +Authorization: Bearer +``` + +## Realtime API + +WebSocket 地址为 `/ws/events/`。连接时携带访问令牌,连接成功后服务端发送 `realtime.connected`。 + +客户端可以发送: + +- `comments.subscribe` +- `comments.unsubscribe` + +服务端可能返回: + +- `comments.subscribed` +- `comments.unsubscribed` +- `realtime.error` +``` + +- [ ] **Step 2: Add English guide** + +Create `asp-doc/docs/en/asp/integrations/api/index.md` with equivalent English content. + +- [ ] **Step 3: Link guide in sidebars** + +Add API page under Integrations in both VitePress configs: + +```ts +{text: 'API', link: 'api/'}, +``` + +--- + +### Task 5: Validate and commit + +**Files:** +- All files above + +- [ ] **Step 1: Backend checks** + +Run: + +```powershell +Set-Location -Path 'C:\Code\agentic-soc-platform\backend' +.\.venv\Scripts\python.exe manage.py check +.\.venv\Scripts\python.exe manage.py spectacular --file $env:TEMP\asp-openapi.yaml +``` + +Expected: `check` passes and schema generation exits successfully. + +- [ ] **Step 2: Documentation check** + +Do not run VitePress build unless explicitly requested. Inspect the sidebar and guide files for valid links and consistent zh/en content. + +- [ ] **Step 3: Commit** + +Commit backend and docs changes: + +```powershell +Set-Location -Path 'C:\Code\agentic-soc-platform' +git add backend asp-doc docs/superpowers/plans/2026-07-22-api-documentation.md +git commit -m "feat: add API documentation endpoints" -m "Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>" +``` + +--- + +## Self-review + +- Spec coverage: backend Swagger/OpenAPI endpoints, local UI assets, JWT/API key auth docs, websocket docs-site guidance, no generated schema artifact, and non-breaking response behavior are covered. +- Placeholder scan: no TODO/TBD placeholders remain. +- Type consistency: route names, dependency names, and settings keys match drf-spectacular conventions.