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'
|
||||
'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'
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
{
|
||||
"verbose": true,
|
||||
"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-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",
|
||||
|
|
|
@ -5,7 +5,7 @@ generator client {
|
|||
|
||||
datasource db {
|
||||
provider = "sqlite"
|
||||
url = env("BACKEND_DB_URL")
|
||||
url = "file:./db.sqlite"
|
||||
}
|
||||
|
||||
model Apikey {
|
||||
|
|
|
@ -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)) {
|
||||
|
|
|
@ -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 = {
|
||||
|
|
|
@ -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,
|
||||
|
|
12
yarn.lock
12
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"
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue