Merge branch 'refactor' of github.com:kolplattformen/api into connect_api

This commit is contained in:
Christian Landgren 2020-12-18 19:41:30 +01:00
commit 9bf1198b91
16 changed files with 12615 additions and 83 deletions

1
.gitignore vendored
View File

@ -4,3 +4,4 @@ output.json
.DS_Store
Pods
secrets.yaml
requests

7035
package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

19
packages/api/README.md Normal file
View File

@ -0,0 +1,19 @@
# $kolplattformen API
## Develop
### Run
```npm run dev```
Log all requests through to Skolplattformen:
```LOG_REQUESTS=true npm run dev```
### Tests
```npm test```
Continuously:
```npm test -- --watchAll```

View File

@ -1,7 +1,7 @@
const OpenAPIBackend = require('openapi-backend').default
const express = require('express')
const jwt = require('jsonwebtoken')
const backend = require('./lib/backend')
const { deconstruct, verifyToken, createToken } = require('./lib/credentials')
const app = express()
app.use(express.json())
@ -21,30 +21,40 @@ api.register({
// register security handler for jwt auth
api.registerSecurityHandler('bearerAuth', (c, req, res) => {
const authHeader = c.request.headers['authorization']
if (!authHeader) {
throw new Error('Missing authorization header')
try {
const { cookie } = verifyToken(c)
return cookie
} catch (err) {
const {message, stack} = err
res.status(500).json({message, stack})
}
const token = authHeader.replace('Bearer ', '')
return jwt.verify(token, process.env.JWT_SECRET || 'secret')
})
// register operation handlers
api.register({
login: async (c, req, res) => {
console.log('login initiated', c.request.query.socialSecurityNumber)
const token = await backend.login(c.request.query.socialSecurityNumber)
return res.status(200).json(token)
try {
console.log('login initiated')
const { socialSecurityNumber } = deconstruct(c)
const token = await backend.login(socialSecurityNumber)
return res.status(200).json(token)
} catch (err) {
return res.status(err.status || 500).json({ message: err.message, stack: err.stack })
}
},
waitForToken: async (c, req, res) => {
const order = c.request.params.order
console.log('wait for token')
const { order } = deconstruct(c)
const cookie = await backend.waitForToken({order})
const jwtToken = jwt.sign(cookie, process.env.JWT_SECRET || 'secret')
const token = createToken(cookie)
console.log('login succeeded')
return res.status(200).json(jwtToken)
return res.status(200).json({token})
},
getChildren: async (c, req, res) => {
const cookie = c.security.bearerAuth
console.log('get children')
const { cookie } = deconstruct(c)
try {
const children = await backend.getChildren(cookie)
return res.status(200).json(children)
@ -53,8 +63,7 @@ api.register({
}
},
getChildById: async (c, req, res) => {
const cookie = c.security.bearerAuth
const childId = c.request.params.childId
const { cookie, childId } = deconstruct(c)
try {
const child = await backend.getChildById(childId, cookie)
return res.status(200).json(child)
@ -63,44 +72,37 @@ api.register({
}
},
getNews: async (c, req, res) => {
const cookie = c.security.bearerAuth
const childId = c.request.params.childId
const { cookie, childId } = deconstruct(c)
const news = await backend.getNews(childId, cookie)
return res.status(200).json(news)
},
getCalendar: async (c, req, res) => {
const cookie = c.security.bearerAuth
const childId = c.request.params.childId
const { cookie, childId } = deconstruct(c)
const calendar = await backend.getCalendar(childId, cookie)
return res.status(200).json(calendar)
},
getNotifications: async (c, req, res) => {
const cookie = c.security.bearerAuth
const childId = c.request.params.childSdsId
const notifications = await backend.getNotifications(childId, cookie)
const { cookie, childSdsId } = deconstruct(c)
const notifications = await backend.getNotifications(childSdsId, cookie)
return res.status(200).json(notifications)
},
getMenu: async (c, req, res) => {
const cookie = c.security.bearerAuth
const childId = c.request.params.childId
const { cookie, childId } = deconstruct(c)
const menu = await backend.getMenu(childId, cookie)
return res.status(200).json(menu)
},
getSchedule: async (c, req, res) => {
const cookie = c.security.bearerAuth
const childId = c.request.params.childSdsId
const schedule = await backend.getSchedule(childId, cookie)
const { cookie, childSdsId } = deconstruct(c)
const schedule = await backend.getSchedule(childSdsId, cookie)
return res.status(200).json(schedule)
},
getClassmates: async (c, req, res) => {
const cookie = c.security.bearerAuth
const childId = c.request.params.childSdsId
const classmates = await backend.getClassmates(childId, cookie)
const { cookie, childSdsId } = deconstruct(c)
const classmates = await backend.getClassmates(childSdsId, cookie)
return res.status(200).json(classmates)
},
download: async (c, req, res) => {
const cookie = c.security.bearerAuth
const url = c.request.query.url
const { cookie, url } = deconstruct(c)
const stream = await backend.download(url, cookie)
stream.body.pipe(res.body)
}

View File

@ -1,75 +1,213 @@
const nodeFetch = require('node-fetch')
const fetch = require('fetch-cookie/node-fetch')(nodeFetch)
const moment = require('moment')
const camel = require('camelcase-keys')
const h2m = require('h2m')
const {htmlDecode} = require('js-htmlencode')
const urls = require('./urls')
const download = (url, cookie) => fetch(url, {headers: {cookie, 'user-agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/87.0.4280.87 Safari/537.36'}})
const { fetchJson, fetchText, fetchRaw } = require('./fetch')
const fetchJson = (url, cookie) => {
return fetch(url, {headers: {cookie}})
//.then(res => console.log('fetching', res.url) || res)
.then(res => res.ok ? res : Promise.reject(res.statusText))
.then(res => res.json())
// convert to camelCase
.then(json => camel(json, { deep: true }))
.then(json => json.error ? Promise.reject(json.error) : json.data)
}
const pause = ms => new Promise((resolve) => setTimeout(resolve, ms))
const download = (url, cookie) => fetchRaw(url, cookie)
const login = async (socialSecurityNumber) => {
const url = urls.login(socialSecurityNumber)
const token = await fetch(url).then(res => res.ok ? res.json() : Promise.reject(res.statusText))
const token = await fetchJson(url)
console.log('login started')
return token
}
const waitForToken = async ({order}, tries = 60) => {
if (!tries) return Promise.reject('Timeout')
const status = await fetch(urls.checkStatus(order)).then(res => res.ok ? res.text() : res.statusText)
if (status === 'OK') return await fetch(urls.loginTarget).then(res => res.ok && res.headers.get('set-cookie'))
return pause(1000).then(() => waitForToken({order}, tries--))
const status = await fetchText(urls.checkStatus(order))
if (status === 'OK') {
const result = await fetchRaw(urls.loginTarget)
return result.headers.get('set-cookie')
} else {
await pause(1000)
console.log('retry', tries)
return await waitForToken({order}, tries--)
}
}
const getChildren = (cookie) => fetchJson(urls.children, cookie).then(children => children.map(({name, id, sdsId, status, schoolId}) => ({name, id, sdsId, status, schoolId})))
const getNews = (childId, cookie) => fetchJson(urls.news(childId), cookie)
.then(news => news.newsItems.map(({body, preamble: intro, header, bannerImageUrl: imageUrl, pubDateSE: published, modDateSE: modified}) =>
({header, intro, body: htmlDecode(h2m(body)), modified, published, imageUrl: urls.image(imageUrl) })))
.catch(err => ({err}))
const getChildren = async (cookie) => {
const children = await fetchJson(urls.children, cookie)
return children
.map(({name, id, sdsId, status, schoolId}) =>
({name, id, sdsId, status, schoolId}))
}
const getCalendar = (childId, cookie) => fetchJson(urls.calendar(childId), cookie)
.then(calendar => calendar.map(({title, id, description, location, longEventDateTime: startDate, longEndDateTime: endDate, allDayEvent: allDay}) => ({title, id, description, location, startDate: moment(startDate, 'YYYY-MM-DD hh:mm').toISOString(), endDate: moment(endDate, 'YYYY-MM-DD hh:mm').toISOString(), allDay})))
.catch(err => ({err}))
const getNews = async(childId, cookie) => {
const news = await fetchJson(urls.news(childId), cookie)
return news
.newsItems
.map(({
body,
preamble: intro,
header,
bannerImageUrl: imageUrl,
pubDateSE: published,
modDateSE: modified
}) => ({
header,
intro,
body: htmlDecode(h2m(body)),
modified,
published,
imageUrl: urls.image(imageUrl)
}))
}
const getNotifications = (childId, cookie) => fetchJson(urls.notifications(childId), cookie)
.then(notifications => notifications.map(({notificationMessage: {messages: {message: {messageid: id, messagetext: message, messagetime: dateCreated, linkbackurl: url, sender, category, messagetype: {type: messageType}}} = {}}}) => ({id, sender, dateCreated: moment(dateCreated).toISOString(), message, url, category, messageType})))
.catch(err => console.error(err) || {err})
const getCalendar = async (childId, cookie) => {
const url = urls.calendar(childId)
const calendar = await fetchJson(url, cookie)
const getMenu = (childId, cookie) => fetchJson(urls.menu(childId), cookie).catch(err => ({err}))
const getSchedule = (childId, cookie) => fetchJson(urls.schedule(childId, moment().format('YYYY-MM-DD'), moment().add(7, 'days').format('YYYY-MM-DD')), cookie)
.then(schedule => schedule.map(({title, id, description, location, longEventDateTime: startDate, longEndDateTime: endDate, allDayEvent: allDay, mentor}) => ({title, id, description, location, startDate: moment(startDate, 'YYYY-MM-DD hh:mm').toISOString(), endDate: moment(endDate, 'YYYY-MM-DD hh:mm').toISOString(), allDay, mentor})))
.catch(err => ({err}))
return calendar
.map(({
title,
id,
description,
location,
longEventDateTime: startDate,
longEndDateTime: endDate,
allDayEvent: allDay
}) => ({
title,
id,
description,
location,
startDate: moment(startDate, 'YYYY-MM-DD hh:mm').toISOString(),
endDate: moment(endDate, 'YYYY-MM-DD hh:mm').toISOString(),
allDay
}))
}
const getClassmates = (childId, cookie) => fetchJson(urls.classmates(childId), cookie)
.then(classmates => classmates.map(({sisId, firstname, lastname, location, guardians = [], className}) => ({sisId, firstname, lastname, location, guardians: guardians.map(({emailhome: email, firstname, lastname, telmobile: mobile, address}) => ({email, firstname, lastname, mobile, address})), className})))
.catch(err => ({err}))
const getNotifications = async (childId, cookie) => {
const url = urls.notifications(childId)
const notifications = await fetchJson(url, cookie)
return notifications
.map(({
notificationMessage: {
messages: {
message: {
messageid: id,
messagetext: message,
messagetime: dateCreated,
linkbackurl: url,
sender,
category,
messagetype: {
type: messageType
}
}
} = {}
}
}) => ({
id,
sender,
dateCreated: moment(dateCreated).toISOString(),
message,
url,
category,
messageType
}))
}
const getMenu = async (childId, cookie) => {
const url = urls.menu(childId)
const menu = await fetchJson(url, cookie)
return menu
}
const getSchedule = async (childId, cookie) => {
const from = moment().format('YYYY-MM-DD')
const to = moment().add(7, 'days').format('YYYY-MM-DD')
const url = urls.schedule(childId, from, to)
const schedule = await fetchJson(url, cookie)
return schedule
.map(({
title,
id,
description,
location,
longEventDateTime: startDate,
longEndDateTime: endDate,
allDayEvent: allDay,
mentor
}) => ({
title,
id,
description,
location,
startDate: moment(startDate, 'YYYY-MM-DD hh:mm').toISOString(),
endDate: moment(endDate, 'YYYY-MM-DD hh:mm').toISOString(),
allDay,
mentor
}))
}
const getClassmates = async (childId, cookie) => {
const url = urls.classmates(childId)
const classMates = await fetchJson(url, cookie)
return classmates
.map(({
sisId,
firstname,
lastname,
location,
guardians = [],
className
}) => ({
sisId,
firstname,
lastname,
location,
guardians: guardians.map(({
emailhome: email,
firstname,
lastname,
telmobile: mobile,
address
}) => ({
email,
firstname,
lastname,
mobile,
address
})),
className
}))
}
const getChildById = async (childId, cookie) => {
const children = await getChildren()
const child = children.find(c => c.id == childId)
const [news, calendar, notifications, menu, schedule] = [
const [
news,
calendar,
notifications,
menu,
schedule
] = [
await getNews(childId, cookie),
await getCalendar(childId, cookie),
await getNotifications(child.sdsId, cookie),
await getMenu(child.id, cookie),
await getSchedule(child.id, cookie)]
await getSchedule(child.id, cookie)
]
return {child, news, calendar, notifications, menu, schedule}
return {
child,
news,
calendar,
notifications,
menu,
schedule
}
}
module.exports = {
login,
waitForToken,

View File

@ -0,0 +1,59 @@
const jwt = require('jsonwebtoken')
const moment = require('moment')
const { encrypt, decrypt } = require('./crypto')
const deconstruct = (c) => {
const result = {}
if (c.security && c.security.bearerAuth) {
result.cookie = decrypt(c.security.bearerAuth)
}
if (c.request.headers) {
if (c.request.headers.authorization) {
result.authorization = c.request.headers.authorization.replace('Bearer ', '')
}
}
if (c.request.params) {
result.order = c.request.params.order
result.childId = c.request.params.childId
result.childSdsId = c.request.params.childSdsId
}
if (c.request.query) {
result.socialSecurityNumber = c.request.query.socialSecurityNumber
result.url = c.request.query.url
}
return result
}
const verifyToken = (c) => {
try {
const { authorization } = deconstruct(c)
if (!authorization) {
throw new Error('Missing authorization header')
}
const decoded = jwt.verify(authorization, process.env.JWT_SECRET || 'secret')
return decoded
} catch (err) {
throw err
}
}
const createToken = (cookie) => {
const objectified = cookie
.split('; ')
.map((slug) => slug.split('='))
.reduce((obj, [key, val]) => ({...obj, [key]: val}), {})
const options = {}
if (objectified.expires) {
options.expiresIn = moment(new Date(objectified.expires)).unix() - moment().unix()
}
return jwt.sign({
cookie: encrypt(cookie)
}, process.env.JWT_SECRET || 'secret', options)
}
module.exports = {
createToken,
deconstruct,
verifyToken
}

View File

@ -0,0 +1,99 @@
const jwt = require('jsonwebtoken')
const moment = require('moment')
const { createToken, deconstruct, verifyToken } = require('./credentials')
const { encrypt, decrypt } = require('./crypto')
describe('credentials', () => {
describe('createToken', () => {
let cookie, expires
beforeEach(() => {
process.env.JWT_SECRET = 'correct'
expires = moment().add(1, 'd')
const weirdFormatTimeString = expires.utc().format('ddd, DD-MMM-YYYY HH:mm:ss') + ' GMT'
cookie = `SMSESSION=foobar; path=/; HttpOnly; SameSite=Lax, StockholmEServiceLanguage=1053,Svenska,Language,sv; expires=${weirdFormatTimeString}; path=/`
})
it('creates a jwt', () => {
const token = createToken(cookie)
expect(token.split('.')).toHaveLength(3)
})
it('stores cookie', () => {
const token = createToken(cookie)
const decoded = jwt.verify(token, process.env.JWT_SECRET)
expect(decrypt(decoded.cookie)).toEqual(cookie)
})
it('sets expiry', () => {
const token = createToken(cookie)
const decoded = jwt.verify(token, process.env.JWT_SECRET)
expect(decoded.exp).toEqual(expires.unix())
})
})
describe('verifyToken', () => {
let token, c, cookie
beforeEach(() => {
process.env.JWT_SECRET = 'correct'
cookie = 'SMSESSION=foobar'
token = jwt.sign({ cookie }, process.env.JWT_SECRET)
c = {
request: {
headers: {
authorization: `Bearer ${token}`
}
}
}
})
it('throws if header is missing', () => {
c.request.headers = {}
expect(() => verifyToken(c)).toThrow('Missing authorization header')
})
it('throws if signature is wrong', () => {
token = jwt.sign({ cookie }, 'wrong')
c.request.headers.authorization = `Bearer ${token}`
expect(() => verifyToken(c)).toThrow('invalid signature')
})
it('returns a jwt on successs', () => {
const decoded = verifyToken(c)
expect(decoded.cookie).toEqual(cookie)
})
})
describe('deconstruct', () => {
let c
beforeEach(() => {
c = {
request: {
headers: {
authorization: 'Bearer authorization'
},
params: {
order: 'abc-123',
childId: 'childId',
childSdsId: 'childSdsId'
},
query: {
socialSecurityNumber: '200001019999',
url: 'https://google.com'
},
},
security: {
bearerAuth: encrypt('bearerAuth')
}
}
})
it('returns all expected values', () => {
expect(deconstruct(c)).toEqual({
authorization: 'authorization',
order: 'abc-123',
childId: 'childId',
childSdsId: 'childSdsId',
socialSecurityNumber: '200001019999',
cookie: 'bearerAuth',
url: 'https://google.com'
})
})
})
})

View File

@ -0,0 +1,51 @@
const {
createCipheriv,
createDecipheriv,
createHash,
randomBytes
} = require('crypto')
const encryptionKey = process.env.JWT_ENCRYPTION_KEY || 'encrypt'
const ALGORITHM = 'aes-256-gcm'
const toBase64Url = (b64) => b64.replace(/\=/g, '').replace(/\+/g, '-').replace(/\\/g, '_')
const fromBase64Url = (b64u) => b64u.replace(/\-/g, '+').replace(/_/g, '\\')
const cipherKey = () => createHash('sha256').update(encryptionKey).digest()
const encrypt = (text) => {
const iv = randomBytes(16)
const key = cipherKey()
const cipherBuffer = Buffer.from(text, 'utf8')
const cipherIv = createCipheriv(ALGORITHM, key, iv)
const [buf1, buf2] = [cipherIv.update(cipherBuffer), cipherIv.final()]
const auth = cipherIv.getAuthTag()
const base64 = Buffer
.concat(
[iv, auth, buf1, buf2],
32 + buf1.length + buf2.length
)
.toString('base64')
return toBase64Url(base64)
}
const decrypt = (encrypted) => {
const buffer = Buffer.from(fromBase64Url(encrypted), 'base64')
const iv = buffer.slice(0, 16)
const auth = buffer.slice(16, 32)
const cipher = buffer.slice(32)
const key = cipherKey()
const decipherIv = createDecipheriv(ALGORITHM, key, iv)
decipherIv.setAuthTag(auth)
const [buf1, buf2] = [decipherIv.update(cipher), decipherIv.final()]
const deciphered = Buffer.concat([buf1, buf2], buf1.length, buf2.length)
return deciphered.toString('utf8')
}
module.exports = {
encrypt,
decrypt
}

View File

@ -0,0 +1,32 @@
const { encrypt, decrypt } = require('./crypto')
describe('crypto', () => {
it('encrypts', () => {
const encrypted = encrypt('message')
expect(typeof encrypted).toBe('string')
})
it('decrypts', () => {
const encrypted = encrypt('message')
const decrypted = decrypt(encrypted)
expect(decrypted).toEqual('message')
})
it('decrypts long texts', () => {
const msg = `
lorem ipsum dolor sit amet, consectetur adipiscing elit
ut aliquam, purus sit amet luctus venenatis, lectus magna
fringilla urna, porttitor rhoncus dolor purus non enim
praesent elementum facilisis leo, vel fringilla est
ullamcorper eget nulla facilisi etiam dignissim diam quis
enim lobortis scelerisque fermentum dui faucibus in ornare
quam viverra orci sagittis eu volutpat odio facilisis
mauris sit amet massa vitae tortor condimentum lacinia
quis vel eros donec ac odio tempor orci dapibus ultrices
in iaculis nunc sed augue lacus, viverra vitae congue eu,
consequat ac felis donec et odio pellentesque diam volutpat
commodo sed egestas egestas fringilla phasellus faucibus
`
const encrypted = encrypt(msg)
const decrypted = decrypt(encrypted)
expect(decrypted).toEqual(msg)
})
})

125
packages/api/lib/fetch.js Normal file
View File

@ -0,0 +1,125 @@
const nodeFetch = require('node-fetch')
const fetch = require('fetch-cookie/node-fetch')(nodeFetch)
const createError = require('http-errors')
const camel = require('camelcase-keys')
const { logRequest } = require('./logger')
const logging = (f) => async (url, cookie) => {
const log = {}
try {
const result = await f(url, cookie, log)
return result
} finally {
if (process.env.LOG_REQUESTS) {
console.error(log)
logRequest(log)
}
}
}
const errorExists = (errors, {message, status}) => {
return !!errors.find((e) => (
e.message === message &&
e.status === status
))
}
const logError = (log, error) => {
if (error) return
const {message, status, stack} = error
if (!log.errors) {
log.errors = [{message, status, stack}]
} else if (!errorExists(error)) {
log.errors.push({message, status, stack})
}
}
const fetchRaw = async (url, cookie, log = {}) => {
const options = {
'user-agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/87.0.4280.87 Safari/537.36'
}
if (cookie) {
options.headers = {cookie}
}
log.request = {
url,
...options
}
const response = await fetch(url, options)
log.response = {
ok: response.ok,
status: response.status,
statusText: response.statusText,
headers: {
'content-type': response.headers.get('content-type')
}
}
if (!response.ok) {
const error = createError(response.status, response.statusText)
logError(log, error)
throw error
}
return response
}
const fetchText = async (url, cookie, log = {}) => {
try {
const response = await fetchRaw(url, cookie, log)
const text = await response.text()
log.response.body = text
return text
} catch (error) {
if (!log.response.body) {
try {
log.response.body = await parse(response)
} catch (_) {}
}
logError(log, error)
throw error
}
}
const fetchJson = async (url, cookie, log = {}) => {
try {
const response = await fetchRaw(url, cookie, log)
if (response.headers.get('content-type').split(';')[0] !== 'application/json') {
log.response.body = await response.text()
throw new Error('Expected JSON')
}
const json = await response.json()
log.response.body = json
const camelized = camel(json, { deep: true })
if (camelized.error) {
throw camelized.error
}
return camelized.data || camelized
} catch (error) {
if (!log.response.body) {
try {
log.response.body = await parse(response)
} catch (_) {}
}
logError(log, error)
throw error
}
}
const parse = async (response) => {
const contentType = response.headers.get('content-type').split(';')[0]
switch (contentType) {
case 'application/json':
const json = await response.json()
return camel(json, { deep: true })
case 'text/html':
return response.text()
default:
return
}
}
module.exports = {
fetchRaw: logging(fetchRaw),
fetchJson: logging(fetchJson),
fetchText: logging(fetchText)
}

View File

@ -0,0 +1,34 @@
const { writeFile } = require('fs/promises')
const { join } = require('path')
const mkdirp = require('mkdirp')
const wait = (ms) => new Promise(r => setTimeout(r, ms))
const logRequest = async (log) => {
try {
const [,,host,...slugs] = log.request.url.split('/')
const [sub] = host.split('.')
const last = slugs.pop()
const filename = []
filename.push((last.indexOf('?') > 0
? last.substring(0, last.indexOf('?'))
: last))
if (log.errors) {
filename.push('_error')
}
filename.push('.json')
const dir = join(process.cwd(), 'requests', sub, ...slugs)
await mkdirp(dir)
await wait(100)
await writeFile(
join(dir, filename.join('')),
JSON.stringify(log, null, 2)
)
} catch (err) {
console.error(err)
}
}
module.exports = {
logRequest
}

View File

@ -0,0 +1,4 @@
{
"verbose": false,
"ignore": ["*.test.js", "fixtures/*", "requests/*"]
}

File diff suppressed because it is too large Load Diff

View File

@ -10,6 +10,7 @@
"express": "^4.17.1",
"fetch-cookie": "^0.11.0",
"h2m": "^0.7.0",
"http-errors": "^1.8.0",
"js-htmlencode": "^0.3.0",
"jsonwebtoken": "^8.5.1",
"moment": "^2.29.1",
@ -17,14 +18,20 @@
"openapi-backend": "^3.6.3"
},
"devDependencies": {
"@types/jest": "^26.0.19",
"@types/jsonwebtoken": "^8.5.0",
"@types/node-fetch": "^2.5.7",
"eslint": "^7.14.0",
"eslint-config-standard": "^16.0.2",
"eslint-plugin-import": "^2.22.1",
"eslint-plugin-node": "^11.1.0",
"eslint-plugin-promise": "^4.2.1"
"eslint-plugin-promise": "^4.2.1",
"jest": "^26.6.3",
"nodemon": "^2.0.6"
},
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
"dev": "nodemon",
"test": "jest"
},
"author": "",
"license": "ISC"

View File

@ -316,8 +316,9 @@ components:
type: object
description: "A JWT token that should be used for authorizing requests"
properties:
jwt:
token:
type: string
example: "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c"
News:
type: array