From 0cdb7a0ae0455b79f7fe2679c41c7229a1e2014d Mon Sep 17 00:00:00 2001 From: joostdecock Date: Sun, 18 Dec 2022 20:04:52 +0100 Subject: [PATCH] wip(backend): Work on OpenAPI v3 spec and swagger docs --- config/dependencies.yaml | 2 + sites/backend/nodemon.json | 2 +- sites/backend/openapi/apikeys.mjs | 205 +++++++++++++++++++++++++++++ sites/backend/openapi/index.mjs | 47 +++++++ sites/backend/openapi/patterns.mjs | 0 sites/backend/openapi/people.mjs | 0 sites/backend/openapi/refs.mjs | 3 + sites/backend/openapi/users.mjs | 0 sites/backend/package.json | 4 +- sites/backend/prisma/schema.prisma | 2 +- sites/backend/scripts/newdb.mjs | 4 +- sites/backend/src/config.mjs | 9 +- sites/backend/src/index.mjs | 4 + yarn.lock | 12 ++ 14 files changed, 283 insertions(+), 11 deletions(-) create mode 100644 sites/backend/openapi/apikeys.mjs create mode 100644 sites/backend/openapi/index.mjs create mode 100644 sites/backend/openapi/patterns.mjs create mode 100644 sites/backend/openapi/people.mjs create mode 100644 sites/backend/openapi/refs.mjs create mode 100644 sites/backend/openapi/users.mjs diff --git a/config/dependencies.yaml b/config/dependencies.yaml index f94966763ed..484d487b639 100644 --- a/config/dependencies.yaml +++ b/config/dependencies.yaml @@ -217,6 +217,8 @@ backend: 'passport-jwt': '4.0.0' 'pino': '8.7.0' 'qrcode': '1.5.1' + 'swagger-ui-dist': '4.15.5' + 'swagger-ui-express': '4.6.0' dev: 'chai': *chai 'chai-http': '4.3.0' diff --git a/sites/backend/nodemon.json b/sites/backend/nodemon.json index 4c9390fe930..7d4f26382d0 100644 --- a/sites/backend/nodemon.json +++ b/sites/backend/nodemon.json @@ -1,5 +1,5 @@ { "verbose": true, "ignore": ["tests/**.test.mjs"], - "watch": ["src/**"] + "watch": ["src/**", "openapi/*"] } diff --git a/sites/backend/openapi/apikeys.mjs b/sites/backend/openapi/apikeys.mjs new file mode 100644 index 00000000000..152cad19e2d --- /dev/null +++ b/sites/backend/openapi/apikeys.mjs @@ -0,0 +1,205 @@ +const tags = ['API Keys'] +const jwt = [{ jwt: [] }] +const key = [{ key: [] }] + +const fields = { + level: { + description: ` +One of the [API permission +levels](https://freesewing.dev/reference/backend/api/rbac#permission-levels) which +is an integer between (and including) \`0\` and \`8\`.`, + type: 'number', + example: 5, + }, + name: { + description: ` +The name of the API key exists solely to help you differentiate between your API keys.`, + type: 'string', + example: 'My first API key', + }, +} + +export const apikeys = {} + +// Create API key - JWT +apikeys['/apikeys/{auth}'] = { + post: { + tags, + security: jwt, + summary: 'Create a new API key', + description: ` +eates a new API key and returns it. +quires a \`name\`, \`level\`, and \`expiresIn\` field in the POST body.`, + parameters: [ + { + in: 'path', + name: 'auth', + required: true, + schema: { + type: 'string', + enum: ['jwt', 'key'], + }, + description: + 'One of `jwt` or `key` depending on whether you want to authentication with a JSON Web Token (`jwt`) or an API key (`key`)', + }, + ], + requestBody: { + required: true, + content: { + 'application/json': { + schema: { + type: 'object', + properties: { + expiresIn: { + description: ` +mber of seconds the API key will remain valid before expiring. +n never be higher than the \`apikeys.maxExpirySeconds\` configuration setting.`, + type: 'number', + example: 3600, + }, + level: fields.level, + name: fields.name, + }, + }, + }, + }, + }, + responses: { + 201: { + description: + '**Success - API key created**\n\n' + + 'Status code `201` indicates that the resource was created successfully.', + content: { + 'application/json': { + schema: { + type: 'object', + properties: { + result: { + description: 'Textual description of the result of the API call', + type: 'string', + example: 'created', + }, + apikey: { + description: 'Object holding the data of the created API key', + type: 'object', + properties: { + expiresAt: { + description: 'UTC Timestamp in ISO 8601 format.', + type: 'string', + example: '2022-12-18T18:14:30.460Z', + }, + key: { + description: 'The _key_ part of the API key serves as the username', + type: 'string', + example: 'c00475bd-3002-4baa-80ad-0145cd6a646c', + }, + level: fields.level, + name: fields.name, + secret: { + description: ` + The _secret_ part of the API key serves as the password. + It is only revealed in the response of the API key creation.`, + type: 'string', + example: '56b74b5dc2da7a4f37b3c9a6172e840cf4912dc37cbc55c87485f2e0abf59245', + }, + userId: { + description: `The unique ID of the user who owns this resource.`, + type: 'number', + example: 4, + }, + }, + }, + }, + }, + }, + }, + }, + 400: { + description: + '**Client error - Invalid request**\n\n' + + 'Status code `400` indicates that the request was invalid.
' + + 'The return body will have an `error` field which can hold:\n\n' + + '- `postBodyMissing` : The POST request did not have a body\n' + + '- `nameMissing` : The `name` field was missing from the request body\n' + + '- `levelMissing` : The `level` field was missing from the request body\n' + + '- `expiresInMissing` : The `expiresIn` field was missing from the request body\n' + + '- `levelNotNumeric` : The `level` field in the request body was a number\n' + + '- `invalidLevel` : The `level` field in the request body was not a valid permission level\n' + + '- `expiresInNotNumeric` : The `expiresIn` field in the request body was not a number\n' + + '- `expiresIsHigherThanMaximum` : The `expiresIn` field in the request body is higher than allowed by the `apikeys.maxExpirySeconds` configuration.' + + '- `keyLevelExceedsRoleLevel` : The `level` field in the request body is higher than the `level` of the user creating the key. Which is not allowed.' + + '', + content: { + 'application/json': { + schema: { + type: 'object', + properties: { + result: { + description: 'Textual description of the result of the API call', + type: 'string', + example: 'error', + }, + error: { + description: 'Textual description of the error that caused this API call to fail', + type: 'string', + example: 'levelMissing', + }, + }, + }, + }, + }, + }, + 500: { + description: + '**Server error - API call failed**\n\n' + + 'Status code `500` indicates that the request could not be handled due to an unforseen error.
' + + 'The return body will have an `error` field which can hold:\n\n' + + '- `createApikeyFailed` : The API key could not be created\n' + + '', + content: { + 'application/json': { + schema: { + type: 'object', + properties: { + result: { + description: 'Textual description of the result of the API call', + type: 'string', + example: 'error', + }, + error: { + description: 'Textual description of the error that caused this API call to fail', + type: 'string', + example: 'createApikeyFailed', + }, + }, + }, + }, + }, + }, + }, + }, +} + +/* + + // Read Apikey + app.get('/apikeys/:id/jwt', passport.authenticate(...jwt), (req, res) => + Apikeys.read(req, res, tools) + ) + app.get('/apikeys/:id/key', passport.authenticate(...bsc), (req, res) => + Apikeys.read(req, res, tools) + ) + + // Read current Apikey + app.get('/whoami/key', passport.authenticate(...bsc), (req, res) => + Apikeys.whoami(req, res, tools) + ) + + // Remove Apikey + app.delete('/apikeys/:id/jwt', passport.authenticate(...jwt), (req, res) => + Apikeys.delete(req, res, tools) + ) + app.delete('/apikeys/:id/key', passport.authenticate(...bsc), (req, res) => + Apikeys.delete(req, res, tools) + ) +*/ diff --git a/sites/backend/openapi/index.mjs b/sites/backend/openapi/index.mjs new file mode 100644 index 00000000000..e7e8816720a --- /dev/null +++ b/sites/backend/openapi/index.mjs @@ -0,0 +1,47 @@ +import pkg from '../package.json' assert { type: 'json' } +import { apikeys } from './apikeys.mjs' + +const description = ` +## What am I looking at? 🤔 +This is reference documentation of the FreeSewing backend API. +It is auto-generated from this API's OpenAPI v3 specification. + +For more documentation and info about this backend, please +visit [freesewing.dev/reference/backend](https://freesewing.dev/reference/backend). + +To learn more about FreeSewing in general, visit [FreeSewing.org](https://freesewing.org/). +` + +export const openapi = { + openapi: '3.0.3', + info: { + title: 'FreeSewing backend API', + version: pkg.version, + description, + contact: { + name: 'Joost De Cock', + email: 'joost@joost.at', + }, + externalDocs: { + description: 'Backend documentation on FreeSewing.dev', + url: 'https://freesewing.dev/reference/backend/', + }, + }, + components: { + securitySchemes: { + jwt: { + type: 'http', + scheme: 'bearer', + bearerFormat: 'JWT', + }, + key: { + type: 'http', + scheme: 'basic', + }, + }, + schemas: {}, + }, + paths: { + ...apikeys, + }, +} diff --git a/sites/backend/openapi/patterns.mjs b/sites/backend/openapi/patterns.mjs new file mode 100644 index 00000000000..e69de29bb2d diff --git a/sites/backend/openapi/people.mjs b/sites/backend/openapi/people.mjs new file mode 100644 index 00000000000..e69de29bb2d diff --git a/sites/backend/openapi/refs.mjs b/sites/backend/openapi/refs.mjs new file mode 100644 index 00000000000..4eace694a54 --- /dev/null +++ b/sites/backend/openapi/refs.mjs @@ -0,0 +1,3 @@ +export const apiLevel = { + name: 'level', +} diff --git a/sites/backend/openapi/users.mjs b/sites/backend/openapi/users.mjs new file mode 100644 index 00000000000..e69de29bb2d diff --git a/sites/backend/package.json b/sites/backend/package.json index e53ce647974..0ad51214665 100644 --- a/sites/backend/package.json +++ b/sites/backend/package.json @@ -41,7 +41,9 @@ "passport-http": "0.3.0", "passport-jwt": "4.0.0", "pino": "8.7.0", - "qrcode": "1.5.1" + "qrcode": "1.5.1", + "swagger-ui-dist": "4.15.5", + "swagger-ui-express": "4.6.0" }, "devDependencies": { "chai": "^4.2.0", diff --git a/sites/backend/prisma/schema.prisma b/sites/backend/prisma/schema.prisma index 7bb057a31df..59775f284d8 100644 --- a/sites/backend/prisma/schema.prisma +++ b/sites/backend/prisma/schema.prisma @@ -5,7 +5,7 @@ generator client { datasource db { provider = "sqlite" - url = env("BACKEND_DB_URL") + url = "file:./db.sqlite" } model Apikey { diff --git a/sites/backend/scripts/newdb.mjs b/sites/backend/scripts/newdb.mjs index 60d7eb595b9..be6b1309243 100644 --- a/sites/backend/scripts/newdb.mjs +++ b/sites/backend/scripts/newdb.mjs @@ -5,11 +5,11 @@ import { banner } from '../../../scripts/banner.mjs' import dotenv from 'dotenv' dotenv.config() +//const DBURL = process.env.BACKEND_DB_URL || 'file://./db.sqlite' const newDb = () => { // Say hi console.log(banner + '\n') - const db = process.env.BACKEND_DB_URL.slice(6) - console.log(db) + const db = process.env.BACKEND_DB_URL ? process.env.BACKEND_DB_URL.slice(6) : './db.sqlite' const schema = path.resolve('./prisma/schema.sqlite') try { if (fs.existsSync(db)) { diff --git a/sites/backend/src/config.mjs b/sites/backend/src/config.mjs index b564e14abb9..ab1d0dfeae1 100644 --- a/sites/backend/src/config.mjs +++ b/sites/backend/src/config.mjs @@ -80,6 +80,9 @@ const baseConfig = { }, base: 'user', }, + tests: { + domain: process.env.BACKEND_TEST_DOMAIN || 'freesewing.dev', + }, website: { domain: process.env.BACKEND_WEBSITE_DOMAIN || 'freesewing.org', scheme: process.env.BACKEND_WEBSITE_SCHEME || 'https', @@ -124,12 +127,6 @@ if (baseConfig.use.github) }, } -// Unit test config -if (baseConfig.use.tests.base) - baseConfig.tests = { - domain: process.env.BACKEND_TEST_DOMAIN || 'freesewing.dev', - } - // Sanity config if (baseConfig.use.sanity) baseConfig.sanity = { diff --git a/sites/backend/src/index.mjs b/sites/backend/src/index.mjs index ceb4a0f0af9..e04f596ef23 100644 --- a/sites/backend/src/index.mjs +++ b/sites/backend/src/index.mjs @@ -17,6 +17,9 @@ import { encryption } from './utils/crypto.mjs' import { mfa } from './utils/mfa.mjs' // Email import { mailer } from './utils/email.mjs' +// Swagger +import swaggerUi from 'swagger-ui-express' +import { openapi } from '../openapi/index.mjs' // Bootstrap const config = verifyConfig() @@ -24,6 +27,7 @@ const prisma = new PrismaClient() const app = express() app.use(express.json()) app.use(express.static('public')) +app.use('/docs', swaggerUi.serve, swaggerUi.setup(openapi)) const tools = { app, diff --git a/yarn.lock b/yarn.lock index 9a497beeaa7..b40fb362f15 100644 --- a/yarn.lock +++ b/yarn.lock @@ -22667,6 +22667,18 @@ svgo@^1.0.0: unquote "~1.1.1" util.promisify "~1.0.0" +swagger-ui-dist@4.15.5, swagger-ui-dist@>=4.11.0: + version "4.15.5" + resolved "https://registry.yarnpkg.com/swagger-ui-dist/-/swagger-ui-dist-4.15.5.tgz#cda226a79db2a9192579cc1f37ec839398a62638" + integrity sha512-V3eIa28lwB6gg7/wfNvAbjwJYmDXy1Jo1POjyTzlB6wPcHiGlRxq39TSjYGVjQrUSAzpv+a7nzp7mDxgNy57xA== + +swagger-ui-express@4.6.0: + version "4.6.0" + resolved "https://registry.yarnpkg.com/swagger-ui-express/-/swagger-ui-express-4.6.0.tgz#fc297d80c614c80f5d7def3dab50b56428cfe1c9" + integrity sha512-ZxpQFp1JR2RF8Ar++CyJzEDdvufa08ujNUJgMVTMWPi86CuQeVdBtvaeO/ysrz6dJAYXf9kbVNhWD7JWocwqsA== + dependencies: + swagger-ui-dist ">=4.11.0" + symbol-observable@1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/symbol-observable/-/symbol-observable-1.0.1.tgz#8340fc4702c3122df5d22288f88283f513d3fdd4"