feat: add OpenAPI 3.1 documentation with Scalar UI
- Add openapi.json spec covering all 59 API routes (~95 operations) - Serve spec at GET /api/docs (no auth required, cached) - Add interactive Scalar API reference UI at /docs - Allow unauthenticated access to /api/docs and /docs in middleware - Add @scalar/api-reference-react dependency - Add 3 E2E tests for spec validation and auth bypass
This commit is contained in:
parent
4489a5f715
commit
4c7b3257d6
File diff suppressed because it is too large
Load Diff
|
|
@ -1,7 +1,7 @@
|
||||||
{
|
{
|
||||||
"name": "mission-control",
|
"name": "mission-control",
|
||||||
"version": "1.2.0",
|
"version": "1.2.0",
|
||||||
"description": "OpenClaw Mission Control \u2014 open-source agent orchestration dashboard",
|
"description": "OpenClaw Mission Control — open-source agent orchestration dashboard",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "next dev --hostname 127.0.0.1",
|
"dev": "next dev --hostname 127.0.0.1",
|
||||||
"build": "next build",
|
"build": "next build",
|
||||||
|
|
@ -16,6 +16,7 @@
|
||||||
"quality:gate": "pnpm test:all"
|
"quality:gate": "pnpm test:all"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@scalar/api-reference-react": "^0.8.66",
|
||||||
"@xyflow/react": "^12.10.0",
|
"@xyflow/react": "^12.10.0",
|
||||||
"autoprefixer": "^10.4.20",
|
"autoprefixer": "^10.4.20",
|
||||||
"better-sqlite3": "^12.6.2",
|
"better-sqlite3": "^12.6.2",
|
||||||
|
|
|
||||||
2577
pnpm-lock.yaml
2577
pnpm-lock.yaml
File diff suppressed because it is too large
Load Diff
|
|
@ -0,0 +1,19 @@
|
||||||
|
import { NextResponse } from 'next/server'
|
||||||
|
import { readFileSync } from 'fs'
|
||||||
|
import { join } from 'path'
|
||||||
|
|
||||||
|
let cachedSpec: string | null = null
|
||||||
|
|
||||||
|
export async function GET() {
|
||||||
|
if (!cachedSpec) {
|
||||||
|
const specPath = join(process.cwd(), 'openapi.json')
|
||||||
|
cachedSpec = readFileSync(specPath, 'utf-8')
|
||||||
|
}
|
||||||
|
|
||||||
|
return new NextResponse(cachedSpec, {
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
'Cache-Control': 'public, max-age=3600',
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,27 @@
|
||||||
|
'use client'
|
||||||
|
|
||||||
|
import { ApiReferenceReact } from '@scalar/api-reference-react'
|
||||||
|
import '@scalar/api-reference-react/style.css'
|
||||||
|
|
||||||
|
export default function DocsPage() {
|
||||||
|
return (
|
||||||
|
<div className="h-screen">
|
||||||
|
<ApiReferenceReact
|
||||||
|
configuration={{
|
||||||
|
url: '/api/docs',
|
||||||
|
theme: 'kepler',
|
||||||
|
darkMode: true,
|
||||||
|
hideModels: false,
|
||||||
|
hideDownloadButton: false,
|
||||||
|
defaultHttpClient: {
|
||||||
|
targetKey: 'shell',
|
||||||
|
clientKey: 'curl',
|
||||||
|
},
|
||||||
|
metaData: {
|
||||||
|
title: 'Mission Control API Docs',
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
@ -91,8 +91,8 @@ export function middleware(request: NextRequest) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Allow login page and auth API without session
|
// Allow login page, auth API, and docs without session
|
||||||
if (pathname === '/login' || pathname.startsWith('/api/auth/')) {
|
if (pathname === '/login' || pathname.startsWith('/api/auth/') || pathname === '/api/docs' || pathname === '/docs') {
|
||||||
return applySecurityHeaders(NextResponse.next())
|
return applySecurityHeaders(NextResponse.next())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,36 @@
|
||||||
|
import { test, expect } from '@playwright/test'
|
||||||
|
|
||||||
|
test.describe('OpenAPI Documentation', () => {
|
||||||
|
test('GET /api/docs returns valid OpenAPI 3.1 JSON', async ({ request }) => {
|
||||||
|
const res = await request.get('/api/docs')
|
||||||
|
expect(res.status()).toBe(200)
|
||||||
|
expect(res.headers()['content-type']).toContain('application/json')
|
||||||
|
|
||||||
|
const spec = await res.json()
|
||||||
|
expect(spec.openapi).toMatch(/^3\.1/)
|
||||||
|
expect(spec.info).toBeDefined()
|
||||||
|
expect(spec.info.title).toBe('Mission Control API')
|
||||||
|
expect(spec.info.version).toBeDefined()
|
||||||
|
expect(spec.paths).toBeDefined()
|
||||||
|
})
|
||||||
|
|
||||||
|
test('GET /api/docs includes key paths', async ({ request }) => {
|
||||||
|
const res = await request.get('/api/docs')
|
||||||
|
const spec = await res.json()
|
||||||
|
|
||||||
|
// Verify core paths exist
|
||||||
|
const paths = Object.keys(spec.paths)
|
||||||
|
expect(paths).toContain('/api/agents')
|
||||||
|
expect(paths).toContain('/api/tasks')
|
||||||
|
expect(paths).toContain('/api/tokens')
|
||||||
|
expect(paths).toContain('/api/auth/login')
|
||||||
|
})
|
||||||
|
|
||||||
|
test('GET /api/docs is accessible without auth', async ({ request }) => {
|
||||||
|
// No API key header, no session cookie
|
||||||
|
const res = await request.get('/api/docs', { headers: {} })
|
||||||
|
expect(res.status()).toBe(200)
|
||||||
|
const spec = await res.json()
|
||||||
|
expect(spec.openapi).toBeDefined()
|
||||||
|
})
|
||||||
|
})
|
||||||
Loading…
Reference in New Issue