import endpoints from '../endpoints.js'; import config from '@/config/index.js'; import { errors as basicErrors } from './errors.js'; import { schemas, convertSchemaToOpenApiSchema } from './schemas.js'; export function genOpenapiSpec() { const spec = { openapi: '3.0.0', info: { version: 'v1', title: 'Calckey API', 'x-logo': { url: '/static-assets/api-doc.png' }, }, externalDocs: { description: 'Repository', url: 'https://codeberg.org/calckey/calckey', }, servers: [{ url: config.apiUrl, }], paths: {} as any, components: { schemas: schemas, securitySchemes: { ApiKeyAuth: { type: 'apiKey', in: 'body', name: 'i', }, // TODO: change this to oauth2 when the remaining oauth stuff is set up Bearer: { type: 'http', scheme: 'bearer', } }, }, }; 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.file = { type: 'string', format: 'binary', description: 'The file contents.', }; schema.required.push('file'); } const security = [ { ApiKeyAuth: [], }, { Bearer: [], }, ]; if (!endpoint.meta.requireCredential) { // add this to make authentication optional security.push({}); } const info = { operationId: endpoint.name, summary: endpoint.name, description: desc, externalDocs: { description: 'Source code', url: `https://codeberg.org/calckey/calckey/src/branch/develop/packages/backend/src/server/api/endpoints/${endpoint.name}.ts`, }, tags: endpoint.meta.tags || undefined, security, 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 Calc', 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'], }, }, }, }, }; const path = { post: info, }; if (endpoint.meta.allowGet) { path.get = { ...info }; // API Key authentication is not permitted for GET requests path.get.security = path.get.security.filter(elem => !Object.prototype.hasOwnProperty.call(elem, 'ApiKeyAuth')); } spec.paths['/' + endpoint.name] = path; } return spec; }