Merge branch 'refactor' of github.com:kolplattformen/api into connect_api
This commit is contained in:
commit
9bf1198b91
|
@ -4,3 +4,4 @@ output.json
|
|||
.DS_Store
|
||||
Pods
|
||||
secrets.yaml
|
||||
requests
|
||||
|
|
File diff suppressed because it is too large
Load Diff
|
@ -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```
|
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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
|
||||
}
|
|
@ -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'
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
|
@ -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
|
||||
}
|
|
@ -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)
|
||||
})
|
||||
})
|
|
@ -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)
|
||||
}
|
|
@ -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
|
||||
}
|
|
@ -0,0 +1,4 @@
|
|||
{
|
||||
"verbose": false,
|
||||
"ignore": ["*.test.js", "fixtures/*", "requests/*"]
|
||||
}
|
File diff suppressed because it is too large
Load Diff
|
@ -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"
|
||||
|
|
|
@ -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
|
||||
|
|
Loading…
Reference in New Issue