mirror of
https://github.com/misskey-dev/misskey.git
synced 2024-12-24 00:39:32 +09:00
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): DM作成時にメンションも含むように
|
||||||
- enhance(client): フォロー申請のボタンのデザインを改善
|
- enhance(client): フォロー申請のボタンのデザインを改善
|
||||||
|
- enhance(backend): OpenAPIエンドポイントを復旧
|
||||||
|
|
||||||
### Bugfixes
|
### Bugfixes
|
||||||
- ロールで広告を無効にするとadmin/adsでプレビューがでてこない問題を修正
|
- ロールで広告を無効にするとadmin/adsでプレビューがでてこない問題を修正
|
||||||
|
- /api-consoleページにアクセスすると404が出る問題を修正
|
||||||
|
|
||||||
## 13.9.2 (2023/03/06)
|
## 13.9.2 (2023/03/06)
|
||||||
|
|
||||||
|
@ -19,6 +19,6 @@
|
|||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<redoc spec-url="/api.json" expand-responses="200" expand-single-schema-field="true"></redoc>
|
<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>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
@ -2,7 +2,6 @@ import * as fs from 'node:fs';
|
|||||||
import { fileURLToPath } from 'node:url';
|
import { fileURLToPath } from 'node:url';
|
||||||
import { dirname } from 'node:path';
|
import { dirname } from 'node:path';
|
||||||
import { Inject, Injectable } from '@nestjs/common';
|
import { Inject, Injectable } from '@nestjs/common';
|
||||||
import fastifyStatic from '@fastify/static';
|
|
||||||
import rename from 'rename';
|
import rename from 'rename';
|
||||||
import type { Config } from '@/config.js';
|
import type { Config } from '@/config.js';
|
||||||
import type { DriveFile, DriveFilesRepository } from '@/models/index.js';
|
import type { DriveFile, DriveFilesRepository } from '@/models/index.js';
|
||||||
@ -60,11 +59,6 @@ export class FileServerService {
|
|||||||
done();
|
done();
|
||||||
});
|
});
|
||||||
|
|
||||||
fastify.register(fastifyStatic, {
|
|
||||||
root: _dirname,
|
|
||||||
serve: false,
|
|
||||||
});
|
|
||||||
|
|
||||||
fastify.get('/files/app-default.jpg', (request, reply) => {
|
fastify.get('/files/app-default.jpg', (request, reply) => {
|
||||||
const file = fs.createReadStream(`${_dirname}/assets/dummy.png`);
|
const file = fs.createReadStream(`${_dirname}/assets/dummy.png`);
|
||||||
reply.header('Content-Type', 'image/jpeg');
|
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 { QueueStatsChannelService } from './api/stream/channels/queue-stats.js';
|
||||||
import { ServerStatsChannelService } from './api/stream/channels/server-stats.js';
|
import { ServerStatsChannelService } from './api/stream/channels/server-stats.js';
|
||||||
import { UserListChannelService } from './api/stream/channels/user-list.js';
|
import { UserListChannelService } from './api/stream/channels/user-list.js';
|
||||||
|
import { OpenApiServerService } from './api/openapi/OpenApiServerService.js';
|
||||||
|
|
||||||
@Module({
|
@Module({
|
||||||
imports: [
|
imports: [
|
||||||
@ -72,6 +73,7 @@ import { UserListChannelService } from './api/stream/channels/user-list.js';
|
|||||||
QueueStatsChannelService,
|
QueueStatsChannelService,
|
||||||
ServerStatsChannelService,
|
ServerStatsChannelService,
|
||||||
UserListChannelService,
|
UserListChannelService,
|
||||||
|
OpenApiServerService,
|
||||||
],
|
],
|
||||||
exports: [
|
exports: [
|
||||||
ServerService,
|
ServerService,
|
||||||
|
@ -1,7 +1,9 @@
|
|||||||
import cluster from 'node:cluster';
|
import cluster from 'node:cluster';
|
||||||
import * as fs from 'node:fs';
|
import * as fs from 'node:fs';
|
||||||
|
import { fileURLToPath } from 'node:url';
|
||||||
import { Inject, Injectable, OnApplicationShutdown } from '@nestjs/common';
|
import { Inject, Injectable, OnApplicationShutdown } from '@nestjs/common';
|
||||||
import Fastify, { FastifyInstance } from 'fastify';
|
import Fastify, { FastifyInstance } from 'fastify';
|
||||||
|
import fastifyStatic from '@fastify/static';
|
||||||
import { IsNull } from 'typeorm';
|
import { IsNull } from 'typeorm';
|
||||||
import { GlobalEventService } from '@/core/GlobalEventService.js';
|
import { GlobalEventService } from '@/core/GlobalEventService.js';
|
||||||
import type { Config } from '@/config.js';
|
import type { Config } from '@/config.js';
|
||||||
@ -21,6 +23,9 @@ import { StreamingApiServerService } from './api/StreamingApiServerService.js';
|
|||||||
import { WellKnownServerService } from './WellKnownServerService.js';
|
import { WellKnownServerService } from './WellKnownServerService.js';
|
||||||
import { FileServerService } from './FileServerService.js';
|
import { FileServerService } from './FileServerService.js';
|
||||||
import { ClientServerService } from './web/ClientServerService.js';
|
import { ClientServerService } from './web/ClientServerService.js';
|
||||||
|
import { OpenApiServerService } from './api/openapi/OpenApiServerService.js';
|
||||||
|
|
||||||
|
const _dirname = fileURLToPath(new URL('.', import.meta.url));
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class ServerService implements OnApplicationShutdown {
|
export class ServerService implements OnApplicationShutdown {
|
||||||
@ -42,6 +47,7 @@ export class ServerService implements OnApplicationShutdown {
|
|||||||
|
|
||||||
private userEntityService: UserEntityService,
|
private userEntityService: UserEntityService,
|
||||||
private apiServerService: ApiServerService,
|
private apiServerService: ApiServerService,
|
||||||
|
private openApiServerService: OpenApiServerService,
|
||||||
private streamingApiServerService: StreamingApiServerService,
|
private streamingApiServerService: StreamingApiServerService,
|
||||||
private activityPubServerService: ActivityPubServerService,
|
private activityPubServerService: ActivityPubServerService,
|
||||||
private wellKnownServerService: WellKnownServerService,
|
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.apiServerService.createServer, { prefix: '/api' });
|
||||||
|
fastify.register(this.openApiServerService.createServer);
|
||||||
fastify.register(this.fileServerService.createServer);
|
fastify.register(this.fileServerService.createServer);
|
||||||
fastify.register(this.activityPubServerService.createServer);
|
fastify.register(this.activityPubServerService.createServer);
|
||||||
fastify.register(this.nodeinfoServerService.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,
|
// Make sure any unknown path under /api returns HTTP 404 Not Found,
|
||||||
// because otherwise ClientServerService will return the base client HTML
|
// because otherwise ClientServerService will return the base client HTML
|
||||||
// page with HTTP 200.
|
// page with HTTP 200.
|
||||||
fastify.get('*', (request, reply) => {
|
fastify.get('/*', (request, reply) => {
|
||||||
reply.code(404);
|
reply.code(404);
|
||||||
// Mock ApiCallService.send's error handling
|
// Mock ApiCallService.send's error handling
|
||||||
reply.send({
|
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
|
//#region static assets
|
||||||
|
|
||||||
fastify.register(fastifyStatic, {
|
|
||||||
root: _dirname,
|
|
||||||
serve: false,
|
|
||||||
});
|
|
||||||
|
|
||||||
fastify.register(fastifyStatic, {
|
fastify.register(fastifyStatic, {
|
||||||
root: staticAssets,
|
root: staticAssets,
|
||||||
prefix: '/static-assets/',
|
prefix: '/static-assets/',
|
||||||
|
@ -13,6 +13,7 @@ const UNSPECIFIED = '*/*';
|
|||||||
// Response Content-Type
|
// Response Content-Type
|
||||||
const AP = 'application/activity+json; charset=utf-8';
|
const AP = 'application/activity+json; charset=utf-8';
|
||||||
const HTML = 'text/html; charset=utf-8';
|
const HTML = 'text/html; charset=utf-8';
|
||||||
|
const JSON_UTF8 = 'application/json; charset=utf-8';
|
||||||
|
|
||||||
describe('Fetch resource', () => {
|
describe('Fetch resource', () => {
|
||||||
let p: INestApplicationContext;
|
let p: INestApplicationContext;
|
||||||
@ -52,14 +53,17 @@ describe('Fetch resource', () => {
|
|||||||
assert.strictEqual(res.type, HTML);
|
assert.strictEqual(res.type, HTML);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('GET api-doc (廃止)', async () => {
|
test('GET api-doc', async () => {
|
||||||
const res = await simpleGet('/api-doc');
|
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');
|
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 () => {
|
test('GET api/foo (存在しない)', async () => {
|
||||||
@ -68,6 +72,12 @@ describe('Fetch resource', () => {
|
|||||||
assert.strictEqual(res.body.error.code, 'UNKNOWN_API_ENDPOINT');
|
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 () => {
|
test('GET favicon.ico', async () => {
|
||||||
const res = await simpleGet('/favicon.ico');
|
const res = await simpleGet('/favicon.ico');
|
||||||
assert.strictEqual(res.status, 200);
|
assert.strictEqual(res.status, 200);
|
||||||
|
Loading…
Reference in New Issue
Block a user