refactor: OpenAPI URL 抓取改为前端执行 + 服务端 CORS 代理
- 前端直接 fetch URL 支持 localhost/内网地址 - CORS 失败自动回退到服务端代理 /api/fetch-spec - 添加 js-yaml 支持 YAML 格式解析 - 服务端移除 specUrl 参数,只接收已解析的 spec 对象 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -6,6 +6,7 @@ import projectRouter from './routes/projects.js';
|
||||
import importRouter from './routes/import.js';
|
||||
import moduleRouter from './routes/modules.js';
|
||||
import endpointRouter from './routes/endpoints.js';
|
||||
import fetchSpecRouter from './routes/fetch-spec.js';
|
||||
|
||||
const app = express();
|
||||
app.use(cors());
|
||||
@@ -18,6 +19,7 @@ app.get('/api/health', (_req, res) => {
|
||||
|
||||
app.use('/api/auth', authRouter);
|
||||
app.use('/api/auth/oauth', oauthRouter);
|
||||
app.use('/api/fetch-spec', fetchSpecRouter);
|
||||
app.use('/api/projects', projectRouter);
|
||||
app.use('/api/projects', importRouter);
|
||||
app.use('/api/projects', moduleRouter);
|
||||
|
||||
30
packages/server/src/routes/fetch-spec.ts
Normal file
30
packages/server/src/routes/fetch-spec.ts
Normal file
@@ -0,0 +1,30 @@
|
||||
import { Router, type Router as RouterType } from 'express';
|
||||
import { requireAuth } from '../middleware/auth.js';
|
||||
|
||||
const router: RouterType = Router();
|
||||
router.use(requireAuth);
|
||||
|
||||
// CORS proxy: frontend calls this when direct fetch is blocked by CORS
|
||||
router.get('/', async (req, res) => {
|
||||
const specUrl = req.query.url as string;
|
||||
if (!specUrl || !specUrl.startsWith('http')) {
|
||||
res.status(400).json({ success: false, error: { code: 'VALIDATION', message: 'Provide a valid URL' } });
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await fetch(specUrl, {
|
||||
headers: { Accept: 'application/json, application/yaml, text/yaml, text/plain, */*' },
|
||||
});
|
||||
if (!response.ok) {
|
||||
res.status(502).json({ success: false, error: { code: 'FETCH_FAILED', message: `Remote server returned ${response.status}` } });
|
||||
return;
|
||||
}
|
||||
const text = await response.text();
|
||||
res.json({ success: true, data: { content: text, contentType: response.headers.get('content-type') || '' } });
|
||||
} catch (err) {
|
||||
res.status(502).json({ success: false, error: { code: 'FETCH_FAILED', message: err instanceof Error ? err.message : 'Failed to fetch URL' } });
|
||||
}
|
||||
});
|
||||
|
||||
export default router;
|
||||
@@ -7,9 +7,9 @@ const router: RouterType = Router();
|
||||
router.use(requireAuth);
|
||||
|
||||
router.post('/:id/reimport', async (req, res) => {
|
||||
const { spec, specUrl } = req.body;
|
||||
if (!spec && !specUrl) {
|
||||
res.status(400).json({ success: false, error: { code: 'VALIDATION', message: 'Provide spec or specUrl' } });
|
||||
const { spec } = req.body;
|
||||
if (!spec) {
|
||||
res.status(400).json({ success: false, error: { code: 'VALIDATION', message: 'Provide spec (JSON or YAML object)' } });
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -22,7 +22,7 @@ router.post('/:id/reimport', async (req, res) => {
|
||||
}
|
||||
|
||||
try {
|
||||
const input = specUrl || spec;
|
||||
const input = spec;
|
||||
const parsed = await parseOpenApiDocument(input);
|
||||
|
||||
await prisma.$transaction(async (tx) => {
|
||||
|
||||
@@ -8,14 +8,14 @@ const router: RouterType = Router();
|
||||
router.use(requireAuth);
|
||||
|
||||
router.post('/', async (req, res) => {
|
||||
const { spec, specUrl } = req.body;
|
||||
if (!spec && !specUrl) {
|
||||
res.status(400).json({ success: false, error: { code: 'VALIDATION', message: 'Provide spec (JSON object) or specUrl (URL string)' } });
|
||||
const { spec } = req.body;
|
||||
if (!spec) {
|
||||
res.status(400).json({ success: false, error: { code: 'VALIDATION', message: 'Provide spec (JSON or YAML object)' } });
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const input = specUrl || spec;
|
||||
const input = spec;
|
||||
const parsed = await parseOpenApiDocument(input);
|
||||
|
||||
const project = await prisma.$transaction(async (tx) => {
|
||||
|
||||
@@ -115,18 +115,8 @@ function parseOpenApi3Endpoints(api: OpenApiDoc): { endpoints: ParsedEndpoint[];
|
||||
}
|
||||
|
||||
export async function parseOpenApiDocument(input: string | object): Promise<ParseResult> {
|
||||
let specInput: string | object = input;
|
||||
|
||||
// If input is a URL, fetch the content first so that swagger-parser
|
||||
// works on a plain object and doesn't need network access for $ref resolution
|
||||
if (typeof input === 'string' && input.startsWith('http')) {
|
||||
const res = await fetch(input);
|
||||
if (!res.ok) throw new Error(`Failed to fetch spec from URL: ${res.status} ${res.statusText}`);
|
||||
specInput = await res.json() as object;
|
||||
}
|
||||
|
||||
// Bundle resolves all $refs into a single document, then dereference inlines them
|
||||
const bundled = await SwaggerParser.bundle(specInput as any) as OpenAPI.Document;
|
||||
// SwaggerParser.bundle handles URLs, JSON objects, and YAML strings natively
|
||||
const bundled = await SwaggerParser.bundle(input as any) as OpenAPI.Document;
|
||||
const api = await SwaggerParser.dereference(bundled, {
|
||||
dereference: { circular: 'ignore' },
|
||||
}) as OpenAPI.Document;
|
||||
|
||||
Reference in New Issue
Block a user