1
0
Fork 0

wip(backend): Work on OpenAPI v3 spec and swagger docs

This commit is contained in:
joostdecock 2022-12-18 20:04:52 +01:00
parent 86cf8787e5
commit 0cdb7a0ae0
14 changed files with 283 additions and 11 deletions

View file

@ -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'

View file

@ -1,5 +1,5 @@
{
"verbose": true,
"ignore": ["tests/**.test.mjs"],
"watch": ["src/**"]
"watch": ["src/**", "openapi/*"]
}

View file

@ -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.<br>' +
'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.<br>' +
'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)
)
*/

View file

@ -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,
},
}

View file

View file

View file

@ -0,0 +1,3 @@
export const apiLevel = {
name: 'level',
}

View file

View file

@ -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",

View file

@ -5,7 +5,7 @@ generator client {
datasource db {
provider = "sqlite"
url = env("BACKEND_DB_URL")
url = "file:./db.sqlite"
}
model Apikey {

View file

@ -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)) {

View file

@ -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 = {

View file

@ -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,

View file

@ -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"