wip(backend): Work on OpenAPI v3 spec and swagger docs
This commit is contained in:
parent
86cf8787e5
commit
0cdb7a0ae0
14 changed files with 283 additions and 11 deletions
|
@ -217,6 +217,8 @@ backend:
|
||||||
'passport-jwt': '4.0.0'
|
'passport-jwt': '4.0.0'
|
||||||
'pino': '8.7.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'
|
||||||
dev:
|
dev:
|
||||||
'chai': *chai
|
'chai': *chai
|
||||||
'chai-http': '4.3.0'
|
'chai-http': '4.3.0'
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
{
|
{
|
||||||
"verbose": true,
|
"verbose": true,
|
||||||
"ignore": ["tests/**.test.mjs"],
|
"ignore": ["tests/**.test.mjs"],
|
||||||
"watch": ["src/**"]
|
"watch": ["src/**", "openapi/*"]
|
||||||
}
|
}
|
||||||
|
|
205
sites/backend/openapi/apikeys.mjs
Normal file
205
sites/backend/openapi/apikeys.mjs
Normal 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)
|
||||||
|
)
|
||||||
|
*/
|
47
sites/backend/openapi/index.mjs
Normal file
47
sites/backend/openapi/index.mjs
Normal 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,
|
||||||
|
},
|
||||||
|
}
|
0
sites/backend/openapi/patterns.mjs
Normal file
0
sites/backend/openapi/patterns.mjs
Normal file
0
sites/backend/openapi/people.mjs
Normal file
0
sites/backend/openapi/people.mjs
Normal file
3
sites/backend/openapi/refs.mjs
Normal file
3
sites/backend/openapi/refs.mjs
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
export const apiLevel = {
|
||||||
|
name: 'level',
|
||||||
|
}
|
0
sites/backend/openapi/users.mjs
Normal file
0
sites/backend/openapi/users.mjs
Normal file
|
@ -41,7 +41,9 @@
|
||||||
"passport-http": "0.3.0",
|
"passport-http": "0.3.0",
|
||||||
"passport-jwt": "4.0.0",
|
"passport-jwt": "4.0.0",
|
||||||
"pino": "8.7.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": {
|
"devDependencies": {
|
||||||
"chai": "^4.2.0",
|
"chai": "^4.2.0",
|
||||||
|
|
|
@ -5,7 +5,7 @@ generator client {
|
||||||
|
|
||||||
datasource db {
|
datasource db {
|
||||||
provider = "sqlite"
|
provider = "sqlite"
|
||||||
url = env("BACKEND_DB_URL")
|
url = "file:./db.sqlite"
|
||||||
}
|
}
|
||||||
|
|
||||||
model Apikey {
|
model Apikey {
|
||||||
|
|
|
@ -5,11 +5,11 @@ import { banner } from '../../../scripts/banner.mjs'
|
||||||
import dotenv from 'dotenv'
|
import dotenv from 'dotenv'
|
||||||
dotenv.config()
|
dotenv.config()
|
||||||
|
|
||||||
|
//const DBURL = process.env.BACKEND_DB_URL || 'file://./db.sqlite'
|
||||||
const newDb = () => {
|
const newDb = () => {
|
||||||
// Say hi
|
// Say hi
|
||||||
console.log(banner + '\n')
|
console.log(banner + '\n')
|
||||||
const db = process.env.BACKEND_DB_URL.slice(6)
|
const db = process.env.BACKEND_DB_URL ? process.env.BACKEND_DB_URL.slice(6) : './db.sqlite'
|
||||||
console.log(db)
|
|
||||||
const schema = path.resolve('./prisma/schema.sqlite')
|
const schema = path.resolve('./prisma/schema.sqlite')
|
||||||
try {
|
try {
|
||||||
if (fs.existsSync(db)) {
|
if (fs.existsSync(db)) {
|
||||||
|
|
|
@ -80,6 +80,9 @@ const baseConfig = {
|
||||||
},
|
},
|
||||||
base: 'user',
|
base: 'user',
|
||||||
},
|
},
|
||||||
|
tests: {
|
||||||
|
domain: process.env.BACKEND_TEST_DOMAIN || 'freesewing.dev',
|
||||||
|
},
|
||||||
website: {
|
website: {
|
||||||
domain: process.env.BACKEND_WEBSITE_DOMAIN || 'freesewing.org',
|
domain: process.env.BACKEND_WEBSITE_DOMAIN || 'freesewing.org',
|
||||||
scheme: process.env.BACKEND_WEBSITE_SCHEME || 'https',
|
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
|
// Sanity config
|
||||||
if (baseConfig.use.sanity)
|
if (baseConfig.use.sanity)
|
||||||
baseConfig.sanity = {
|
baseConfig.sanity = {
|
||||||
|
|
|
@ -17,6 +17,9 @@ import { encryption } from './utils/crypto.mjs'
|
||||||
import { mfa } from './utils/mfa.mjs'
|
import { mfa } from './utils/mfa.mjs'
|
||||||
// Email
|
// Email
|
||||||
import { mailer } from './utils/email.mjs'
|
import { mailer } from './utils/email.mjs'
|
||||||
|
// Swagger
|
||||||
|
import swaggerUi from 'swagger-ui-express'
|
||||||
|
import { openapi } from '../openapi/index.mjs'
|
||||||
|
|
||||||
// Bootstrap
|
// Bootstrap
|
||||||
const config = verifyConfig()
|
const config = verifyConfig()
|
||||||
|
@ -24,6 +27,7 @@ const prisma = new PrismaClient()
|
||||||
const app = express()
|
const app = express()
|
||||||
app.use(express.json())
|
app.use(express.json())
|
||||||
app.use(express.static('public'))
|
app.use(express.static('public'))
|
||||||
|
app.use('/docs', swaggerUi.serve, swaggerUi.setup(openapi))
|
||||||
|
|
||||||
const tools = {
|
const tools = {
|
||||||
app,
|
app,
|
||||||
|
|
12
yarn.lock
12
yarn.lock
|
@ -22667,6 +22667,18 @@ svgo@^1.0.0:
|
||||||
unquote "~1.1.1"
|
unquote "~1.1.1"
|
||||||
util.promisify "~1.0.0"
|
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:
|
symbol-observable@1.0.1:
|
||||||
version "1.0.1"
|
version "1.0.1"
|
||||||
resolved "https://registry.yarnpkg.com/symbol-observable/-/symbol-observable-1.0.1.tgz#8340fc4702c3122df5d22288f88283f513d3fdd4"
|
resolved "https://registry.yarnpkg.com/symbol-observable/-/symbol-observable-1.0.1.tgz#8340fc4702c3122df5d22288f88283f513d3fdd4"
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue