enhance(backend): restore OpenAPI endpoints (#10281)
* enhance(backend): restore OpenAPI endpoints * Update CHANGELOG.md * version * set max-age * update redoc * follow redoc documentation --------- Co-authored-by: tamaina <tamaina@hotmail.co.jp>
This commit is contained in:
parent
caf646fcb0
commit
e0b7633a7a
@ -17,9 +17,11 @@ You should also include the user name that made the change.
|
||||
- ノートごとに絵文字リアクションを受け取るか設定できるように
|
||||
- enhance(client): DM作成時にメンションも含むように
|
||||
- enhance(client): フォロー申請のボタンのデザインを改善
|
||||
- enhance(backend): OpenAPIエンドポイントを復旧
|
||||
|
||||
### Bugfixes
|
||||
- ロールで広告を無効にするとadmin/adsでプレビューがでてこない問題を修正
|
||||
- /api-consoleページにアクセスすると404が出る問題を修正
|
||||
|
||||
## 13.9.2 (2023/03/06)
|
||||
|
||||
|
@ -19,6 +19,6 @@
|
||||
</head>
|
||||
<body>
|
||||
<redoc spec-url="/api.json" expand-responses="200" expand-single-schema-field="true"></redoc>
|
||||
<script src="https://cdn.jsdelivr.net/npm/redoc@2.0.0-rc.50/bundles/redoc.standalone.js" integrity="sha256-WJbngBWN9vp6vkEuzeoSj5tE5saW9Hfj6/SinkzhL2s=" crossorigin="anonymous"></script>
|
||||
<script src="https://cdn.redoc.ly/redoc/latest/bundles/redoc.standalone.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
@ -2,7 +2,6 @@ import * as fs from 'node:fs';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
import { dirname } from 'node:path';
|
||||
import { Inject, Injectable } from '@nestjs/common';
|
||||
import fastifyStatic from '@fastify/static';
|
||||
import rename from 'rename';
|
||||
import type { Config } from '@/config.js';
|
||||
import type { DriveFile, DriveFilesRepository } from '@/models/index.js';
|
||||
@ -60,11 +59,6 @@ export class FileServerService {
|
||||
done();
|
||||
});
|
||||
|
||||
fastify.register(fastifyStatic, {
|
||||
root: _dirname,
|
||||
serve: false,
|
||||
});
|
||||
|
||||
fastify.get('/files/app-default.jpg', (request, reply) => {
|
||||
const file = fs.createReadStream(`${_dirname}/assets/dummy.png`);
|
||||
reply.header('Content-Type', 'image/jpeg');
|
||||
|
@ -33,6 +33,7 @@ import { LocalTimelineChannelService } from './api/stream/channels/local-timelin
|
||||
import { QueueStatsChannelService } from './api/stream/channels/queue-stats.js';
|
||||
import { ServerStatsChannelService } from './api/stream/channels/server-stats.js';
|
||||
import { UserListChannelService } from './api/stream/channels/user-list.js';
|
||||
import { OpenApiServerService } from './api/openapi/OpenApiServerService.js';
|
||||
|
||||
@Module({
|
||||
imports: [
|
||||
@ -72,6 +73,7 @@ import { UserListChannelService } from './api/stream/channels/user-list.js';
|
||||
QueueStatsChannelService,
|
||||
ServerStatsChannelService,
|
||||
UserListChannelService,
|
||||
OpenApiServerService,
|
||||
],
|
||||
exports: [
|
||||
ServerService,
|
||||
|
@ -1,7 +1,9 @@
|
||||
import cluster from 'node:cluster';
|
||||
import * as fs from 'node:fs';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
import { Inject, Injectable, OnApplicationShutdown } from '@nestjs/common';
|
||||
import Fastify, { FastifyInstance } from 'fastify';
|
||||
import fastifyStatic from '@fastify/static';
|
||||
import { IsNull } from 'typeorm';
|
||||
import { GlobalEventService } from '@/core/GlobalEventService.js';
|
||||
import type { Config } from '@/config.js';
|
||||
@ -21,6 +23,9 @@ import { StreamingApiServerService } from './api/StreamingApiServerService.js';
|
||||
import { WellKnownServerService } from './WellKnownServerService.js';
|
||||
import { FileServerService } from './FileServerService.js';
|
||||
import { ClientServerService } from './web/ClientServerService.js';
|
||||
import { OpenApiServerService } from './api/openapi/OpenApiServerService.js';
|
||||
|
||||
const _dirname = fileURLToPath(new URL('.', import.meta.url));
|
||||
|
||||
@Injectable()
|
||||
export class ServerService implements OnApplicationShutdown {
|
||||
@ -42,6 +47,7 @@ export class ServerService implements OnApplicationShutdown {
|
||||
|
||||
private userEntityService: UserEntityService,
|
||||
private apiServerService: ApiServerService,
|
||||
private openApiServerService: OpenApiServerService,
|
||||
private streamingApiServerService: StreamingApiServerService,
|
||||
private activityPubServerService: ActivityPubServerService,
|
||||
private wellKnownServerService: WellKnownServerService,
|
||||
@ -71,7 +77,15 @@ export class ServerService implements OnApplicationShutdown {
|
||||
});
|
||||
}
|
||||
|
||||
// Register non-serving static server so that the child services can use reply.sendFile.
|
||||
// `root` here is just a placeholder and each call must use its own `rootPath`.
|
||||
fastify.register(fastifyStatic, {
|
||||
root: _dirname,
|
||||
serve: false,
|
||||
});
|
||||
|
||||
fastify.register(this.apiServerService.createServer, { prefix: '/api' });
|
||||
fastify.register(this.openApiServerService.createServer);
|
||||
fastify.register(this.fileServerService.createServer);
|
||||
fastify.register(this.activityPubServerService.createServer);
|
||||
fastify.register(this.nodeinfoServerService.createServer);
|
||||
|
@ -167,7 +167,7 @@ export class ApiServerService {
|
||||
// Make sure any unknown path under /api returns HTTP 404 Not Found,
|
||||
// because otherwise ClientServerService will return the base client HTML
|
||||
// page with HTTP 200.
|
||||
fastify.get('*', (request, reply) => {
|
||||
fastify.get('/*', (request, reply) => {
|
||||
reply.code(404);
|
||||
// Mock ApiCallService.send's error handling
|
||||
reply.send({
|
||||
|
@ -0,0 +1,31 @@
|
||||
import { fileURLToPath } from 'node:url';
|
||||
import { Inject, Injectable } from '@nestjs/common';
|
||||
import type { Config } from '@/config.js';
|
||||
import { DI } from '@/di-symbols.js';
|
||||
import { bindThis } from '@/decorators.js';
|
||||
import { genOpenapiSpec } from './gen-spec.js';
|
||||
import type { FastifyInstance, FastifyPluginOptions } from 'fastify';
|
||||
|
||||
const staticAssets = fileURLToPath(new URL('../../../../assets/', import.meta.url));
|
||||
|
||||
@Injectable()
|
||||
export class OpenApiServerService {
|
||||
constructor(
|
||||
@Inject(DI.config)
|
||||
private config: Config,
|
||||
) {
|
||||
}
|
||||
|
||||
@bindThis
|
||||
public createServer(fastify: FastifyInstance, _options: FastifyPluginOptions, done: (err?: Error) => void) {
|
||||
fastify.get('/api-doc', async (_request, reply) => {
|
||||
reply.header('Cache-Control', 'public, max-age=86400');
|
||||
return await reply.sendFile('/redoc.html', staticAssets);
|
||||
});
|
||||
fastify.get('/api.json', (_request, reply) => {
|
||||
reply.header('Cache-Control', 'public, max-age=600');
|
||||
reply.send(genOpenapiSpec(this.config));
|
||||
});
|
||||
done();
|
||||
}
|
||||
}
|
193
packages/backend/src/server/api/openapi/gen-spec.ts
Normal file
193
packages/backend/src/server/api/openapi/gen-spec.ts
Normal file
@ -0,0 +1,193 @@
|
||||
import type { Config } from '@/config.js';
|
||||
import endpoints from '../endpoints.js';
|
||||
import { errors as basicErrors } from './errors.js';
|
||||
import { schemas, convertSchemaToOpenApiSchema } from './schemas.js';
|
||||
|
||||
export function genOpenapiSpec(config: Config) {
|
||||
const spec = {
|
||||
openapi: '3.0.0',
|
||||
|
||||
info: {
|
||||
version: config.version,
|
||||
title: 'Misskey API',
|
||||
'x-logo': { url: '/static-assets/api-doc.png' },
|
||||
},
|
||||
|
||||
externalDocs: {
|
||||
description: 'Repository',
|
||||
url: 'https://github.com/misskey-dev/misskey',
|
||||
},
|
||||
|
||||
servers: [{
|
||||
url: config.apiUrl,
|
||||
}],
|
||||
|
||||
paths: {} as any,
|
||||
|
||||
components: {
|
||||
schemas: schemas,
|
||||
|
||||
securitySchemes: {
|
||||
ApiKeyAuth: {
|
||||
type: 'apiKey',
|
||||
in: 'body',
|
||||
name: 'i',
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
for (const endpoint of endpoints.filter(ep => !ep.meta.secure)) {
|
||||
const errors = {} as any;
|
||||
|
||||
if (endpoint.meta.errors) {
|
||||
for (const e of Object.values(endpoint.meta.errors)) {
|
||||
errors[e.code] = {
|
||||
value: {
|
||||
error: e,
|
||||
},
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
const resSchema = endpoint.meta.res ? convertSchemaToOpenApiSchema(endpoint.meta.res) : {};
|
||||
|
||||
let desc = (endpoint.meta.description ? endpoint.meta.description : 'No description provided.') + '\n\n';
|
||||
desc += `**Credential required**: *${endpoint.meta.requireCredential ? 'Yes' : 'No'}*`;
|
||||
if (endpoint.meta.kind) {
|
||||
const kind = endpoint.meta.kind;
|
||||
desc += ` / **Permission**: *${kind}*`;
|
||||
}
|
||||
|
||||
const requestType = endpoint.meta.requireFile ? 'multipart/form-data' : 'application/json';
|
||||
const schema = { ...endpoint.params };
|
||||
|
||||
if (endpoint.meta.requireFile) {
|
||||
schema.properties = {
|
||||
...schema.properties,
|
||||
file: {
|
||||
type: 'string',
|
||||
format: 'binary',
|
||||
description: 'The file contents.',
|
||||
},
|
||||
};
|
||||
schema.required = [...schema.required ?? [], 'file'];
|
||||
}
|
||||
|
||||
const info = {
|
||||
operationId: endpoint.name,
|
||||
summary: endpoint.name,
|
||||
description: desc,
|
||||
externalDocs: {
|
||||
description: 'Source code',
|
||||
url: `https://github.com/misskey-dev/misskey/blob/develop/packages/backend/src/server/api/endpoints/${endpoint.name}.ts`,
|
||||
},
|
||||
...(endpoint.meta.tags ? {
|
||||
tags: [endpoint.meta.tags[0]],
|
||||
} : {}),
|
||||
...(endpoint.meta.requireCredential ? {
|
||||
security: [{
|
||||
ApiKeyAuth: [],
|
||||
}],
|
||||
} : {}),
|
||||
requestBody: {
|
||||
required: true,
|
||||
content: {
|
||||
[requestType]: {
|
||||
schema,
|
||||
},
|
||||
},
|
||||
},
|
||||
responses: {
|
||||
...(endpoint.meta.res ? {
|
||||
'200': {
|
||||
description: 'OK (with results)',
|
||||
content: {
|
||||
'application/json': {
|
||||
schema: resSchema,
|
||||
},
|
||||
},
|
||||
},
|
||||
} : {
|
||||
'204': {
|
||||
description: 'OK (without any results)',
|
||||
},
|
||||
}),
|
||||
'400': {
|
||||
description: 'Client error',
|
||||
content: {
|
||||
'application/json': {
|
||||
schema: {
|
||||
$ref: '#/components/schemas/Error',
|
||||
},
|
||||
examples: { ...errors, ...basicErrors['400'] },
|
||||
},
|
||||
},
|
||||
},
|
||||
'401': {
|
||||
description: 'Authentication error',
|
||||
content: {
|
||||
'application/json': {
|
||||
schema: {
|
||||
$ref: '#/components/schemas/Error',
|
||||
},
|
||||
examples: basicErrors['401'],
|
||||
},
|
||||
},
|
||||
},
|
||||
'403': {
|
||||
description: 'Forbidden error',
|
||||
content: {
|
||||
'application/json': {
|
||||
schema: {
|
||||
$ref: '#/components/schemas/Error',
|
||||
},
|
||||
examples: basicErrors['403'],
|
||||
},
|
||||
},
|
||||
},
|
||||
'418': {
|
||||
description: 'I\'m Ai',
|
||||
content: {
|
||||
'application/json': {
|
||||
schema: {
|
||||
$ref: '#/components/schemas/Error',
|
||||
},
|
||||
examples: basicErrors['418'],
|
||||
},
|
||||
},
|
||||
},
|
||||
...(endpoint.meta.limit ? {
|
||||
'429': {
|
||||
description: 'To many requests',
|
||||
content: {
|
||||
'application/json': {
|
||||
schema: {
|
||||
$ref: '#/components/schemas/Error',
|
||||
},
|
||||
examples: basicErrors['429'],
|
||||
},
|
||||
},
|
||||
},
|
||||
} : {}),
|
||||
'500': {
|
||||
description: 'Internal server error',
|
||||
content: {
|
||||
'application/json': {
|
||||
schema: {
|
||||
$ref: '#/components/schemas/Error',
|
||||
},
|
||||
examples: basicErrors['500'],
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
spec.paths['/' + endpoint.name] = {
|
||||
post: info,
|
||||
};
|
||||
}
|
||||
|
||||
return spec;
|
||||
}
|
@ -194,11 +194,6 @@ export class ClientServerService {
|
||||
|
||||
//#region static assets
|
||||
|
||||
fastify.register(fastifyStatic, {
|
||||
root: _dirname,
|
||||
serve: false,
|
||||
});
|
||||
|
||||
fastify.register(fastifyStatic, {
|
||||
root: staticAssets,
|
||||
prefix: '/static-assets/',
|
||||
|
@ -13,6 +13,7 @@ const UNSPECIFIED = '*/*';
|
||||
// Response Content-Type
|
||||
const AP = 'application/activity+json; charset=utf-8';
|
||||
const HTML = 'text/html; charset=utf-8';
|
||||
const JSON_UTF8 = 'application/json; charset=utf-8';
|
||||
|
||||
describe('Fetch resource', () => {
|
||||
let p: INestApplicationContext;
|
||||
@ -52,14 +53,17 @@ describe('Fetch resource', () => {
|
||||
assert.strictEqual(res.type, HTML);
|
||||
});
|
||||
|
||||
test('GET api-doc (廃止)', async () => {
|
||||
test('GET api-doc', async () => {
|
||||
const res = await simpleGet('/api-doc');
|
||||
assert.strictEqual(res.status, 404);
|
||||
assert.strictEqual(res.status, 200);
|
||||
// fastify-static gives charset=UTF-8 instead of utf-8 and that's okay
|
||||
assert.strictEqual(res.type?.toLowerCase(), HTML);
|
||||
});
|
||||
|
||||
test('GET api.json (廃止)', async () => {
|
||||
test('GET api.json', async () => {
|
||||
const res = await simpleGet('/api.json');
|
||||
assert.strictEqual(res.status, 404);
|
||||
assert.strictEqual(res.status, 200);
|
||||
assert.strictEqual(res.type, JSON_UTF8);
|
||||
});
|
||||
|
||||
test('GET api/foo (存在しない)', async () => {
|
||||
@ -68,6 +72,12 @@ describe('Fetch resource', () => {
|
||||
assert.strictEqual(res.body.error.code, 'UNKNOWN_API_ENDPOINT');
|
||||
});
|
||||
|
||||
test('GET api-console (client page)', async () => {
|
||||
const res = await simpleGet('/api-console');
|
||||
assert.strictEqual(res.status, 200);
|
||||
assert.strictEqual(res.type, HTML);
|
||||
});
|
||||
|
||||
test('GET favicon.ico', async () => {
|
||||
const res = await simpleGet('/favicon.ico');
|
||||
assert.strictEqual(res.status, 200);
|
||||
|
Loading…
Reference in New Issue
Block a user