Huge update and cleanup on dependencies and test environment among other stuff
This commit is contained in:
parent
8f70c1de67
commit
dc462a3d53
32 changed files with 984 additions and 383 deletions
5
.babelrc
5
.babelrc
|
@ -1,5 +0,0 @@
|
||||||
{
|
|
||||||
"plugins": [
|
|
||||||
"transform-es2015-modules-commonjs"
|
|
||||||
]
|
|
||||||
}
|
|
|
@ -2,31 +2,26 @@ version: 2
|
||||||
jobs:
|
jobs:
|
||||||
build:
|
build:
|
||||||
docker:
|
docker:
|
||||||
- image: docker:latest
|
- image: circleci/node:latest
|
||||||
environment:
|
environment:
|
||||||
- di: "nfpis/storage-upload"
|
- di: "nfpis/storage-upload"
|
||||||
- dtag: "latest"
|
- dtag: "latest"
|
||||||
working_directory: ~/storage-upload
|
working_directory: ~/storage-upload
|
||||||
steps:
|
steps:
|
||||||
- run:
|
|
||||||
name: Update and install SSH & Git
|
|
||||||
command: apk update && apk upgrade && apk add --no-cache bash git openssh
|
|
||||||
- checkout
|
- checkout
|
||||||
- setup_remote_docker
|
|
||||||
- run:
|
- run:
|
||||||
name: Build docker image
|
name: Install dependencies
|
||||||
command: |
|
command: |
|
||||||
docker build -t nfp-test .
|
npm install
|
||||||
docker build --build-arg NODE=production -t ${di}:build_${CIRCLE_BUILD_NUM} -t ${di}:${CIRCLE_SHA1} -t ${di}:${dtag} .
|
|
||||||
- run:
|
- run:
|
||||||
name: Run tests
|
name: Run tests
|
||||||
command: |
|
command: |
|
||||||
docker run nfp-test npm run test
|
npm test
|
||||||
- deploy:
|
# - deploy:
|
||||||
name: Push to docker
|
# name: Push to docker
|
||||||
command: |
|
# command: |
|
||||||
docker login -u $DOCKER_USER -p $DOCKER_PASS
|
# docker login -u $DOCKER_USER -p $DOCKER_PASS
|
||||||
docker push ${di}
|
# docker push ${di}
|
||||||
|
|
||||||
workflows:
|
workflows:
|
||||||
version: 2
|
version: 2
|
||||||
|
|
|
@ -1,6 +1,12 @@
|
||||||
'use strict'
|
import fs from 'fs'
|
||||||
|
import path from 'path'
|
||||||
|
import { fileURLToPath } from 'url'
|
||||||
|
import Nconf from 'nconf-lite'
|
||||||
|
|
||||||
|
const nconf = new Nconf()
|
||||||
|
const __dirname = path.dirname(fileURLToPath(import.meta.url))
|
||||||
|
let pckg = JSON.parse(fs.readFileSync(path.resolve(path.join(__dirname, '../package.json'))))
|
||||||
|
|
||||||
const nconf = require('nconf')
|
|
||||||
|
|
||||||
// Helper method for global usage.
|
// Helper method for global usage.
|
||||||
nconf.inTest = () => nconf.get('NODE_ENV') === 'test'
|
nconf.inTest = () => nconf.get('NODE_ENV') === 'test'
|
||||||
|
@ -12,9 +18,6 @@ nconf.inTest = () => nconf.get('NODE_ENV') === 'test'
|
||||||
// 4. config/config.default.json
|
// 4. config/config.default.json
|
||||||
|
|
||||||
|
|
||||||
// Load package.json for name and such
|
|
||||||
let pckg = require('./package.json')
|
|
||||||
|
|
||||||
pckg = {
|
pckg = {
|
||||||
name: pckg.name,
|
name: pckg.name,
|
||||||
version: pckg.version,
|
version: pckg.version,
|
||||||
|
@ -34,22 +37,22 @@ nconf.env()
|
||||||
|
|
||||||
|
|
||||||
// Load any overrides from the appropriate config file
|
// Load any overrides from the appropriate config file
|
||||||
let configFile = 'config/config.json'
|
let configFile = '../config/config.json'
|
||||||
|
|
||||||
/* istanbul ignore else */
|
/* istanbul ignore else */
|
||||||
if (nconf.get('NODE_ENV') === 'test') {
|
if (nconf.get('NODE_ENV') === 'test') {
|
||||||
configFile = 'config/config.test.json'
|
configFile = '../config/config.test.json'
|
||||||
}
|
}
|
||||||
|
|
||||||
/* istanbul ignore if */
|
/* istanbul ignore if */
|
||||||
if (nconf.get('NODE_ENV') === 'production') {
|
if (nconf.get('NODE_ENV') === 'production') {
|
||||||
configFile = 'config/config.production.json'
|
configFile = '../config/config.production.json'
|
||||||
}
|
}
|
||||||
|
|
||||||
nconf.file('main', configFile)
|
nconf.file('main', path.resolve(path.join(__dirname, configFile)))
|
||||||
|
|
||||||
// Load defaults
|
// Load defaults
|
||||||
nconf.file('default', 'config/config.default.json')
|
nconf.file('default', path.resolve(path.join(__dirname, '../config/config.default.json')))
|
||||||
|
|
||||||
|
|
||||||
// Final sanity checks
|
// Final sanity checks
|
||||||
|
@ -61,4 +64,4 @@ if (typeof global.it === 'function' & !nconf.inTest()) {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
module.exports = nconf
|
export default nconf
|
|
@ -1,20 +0,0 @@
|
||||||
|
|
||||||
function defaults(options, def) {
|
|
||||||
options = options || {}
|
|
||||||
|
|
||||||
Object.keys(def).forEach(function(key) {
|
|
||||||
if (typeof options[key] === 'undefined') {
|
|
||||||
// No need to do clone since we mostly deal with
|
|
||||||
// flat objects
|
|
||||||
options[key] = def[key]
|
|
||||||
}
|
|
||||||
else if (typeof options[key] === 'object' &&
|
|
||||||
typeof def[key] === 'object') {
|
|
||||||
options[key] = defaults(options[key], def[key])
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
return options
|
|
||||||
}
|
|
||||||
|
|
||||||
export default defaults
|
|
34
api/defaults.mjs
Normal file
34
api/defaults.mjs
Normal file
|
@ -0,0 +1,34 @@
|
||||||
|
|
||||||
|
// taken from isobject npm library
|
||||||
|
function isObject(val) {
|
||||||
|
return val != null && typeof val === 'object' && Array.isArray(val) === false
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function defaults(options, def) {
|
||||||
|
let out = { }
|
||||||
|
|
||||||
|
if (options) {
|
||||||
|
Object.keys(options || {}).forEach(key => {
|
||||||
|
out[key] = options[key]
|
||||||
|
|
||||||
|
if (Array.isArray(out[key])) {
|
||||||
|
out[key] = out[key].map(item => {
|
||||||
|
if (isObject(item)) return defaults(item)
|
||||||
|
return item
|
||||||
|
})
|
||||||
|
} else if (out[key] && typeof out[key] === 'object') {
|
||||||
|
out[key] = defaults(options[key], def && def[key])
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
if (def) {
|
||||||
|
Object.keys(def).forEach(function(key) {
|
||||||
|
if (typeof out[key] === 'undefined') {
|
||||||
|
out[key] = def[key]
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
return out
|
||||||
|
}
|
19
api/jwt.js
19
api/jwt.js
|
@ -1,19 +0,0 @@
|
||||||
import jwt from 'jsonwebtoken'
|
|
||||||
|
|
||||||
export function sign(value, secret) {
|
|
||||||
return jwt.sign(value, secret)
|
|
||||||
}
|
|
||||||
|
|
||||||
export function verify(token, secret) {
|
|
||||||
return new Promise((resolve, reject) =>
|
|
||||||
jwt.verify(token, secret, (err, res) => {
|
|
||||||
if (err) return reject(err)
|
|
||||||
|
|
||||||
resolve(res)
|
|
||||||
})
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
export function decode(token) {
|
|
||||||
return jwt.decode(token)
|
|
||||||
}
|
|
21
api/jwt/base64urlsafe.mjs
Normal file
21
api/jwt/base64urlsafe.mjs
Normal file
|
@ -0,0 +1,21 @@
|
||||||
|
|
||||||
|
export function encode(buffer) {
|
||||||
|
return buffer
|
||||||
|
.toString('base64')
|
||||||
|
.replace(/\+/g, '-') // Convert '+' to '-'
|
||||||
|
.replace(/\//g, '_') // Convert '/' to '_'
|
||||||
|
.replace(/=+$/, '') // Remove ending '='
|
||||||
|
}
|
||||||
|
|
||||||
|
export function decode(base64StringUrlSafe) {
|
||||||
|
let base64String = base64StringUrlSafe.replace(/-/g, '+').replace(/_/g, '/')
|
||||||
|
switch (base64String.length % 4) {
|
||||||
|
case 2:
|
||||||
|
base64String += '=='
|
||||||
|
break
|
||||||
|
case 3:
|
||||||
|
base64String += '='
|
||||||
|
break
|
||||||
|
}
|
||||||
|
return Buffer.from(base64String, 'base64')
|
||||||
|
}
|
169
api/jwt/decode.mjs
Normal file
169
api/jwt/decode.mjs
Normal file
|
@ -0,0 +1,169 @@
|
||||||
|
import crypto from 'crypto'
|
||||||
|
import defaults from '../defaults.mjs'
|
||||||
|
import * as base64UrlSafe from './base64urlsafe.mjs'
|
||||||
|
|
||||||
|
const defaultOptions = {
|
||||||
|
expiresSkew: 0,
|
||||||
|
expiresMax: 0,
|
||||||
|
nbfIatSkew: 300,
|
||||||
|
fixup: null
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @param {string} jwt
|
||||||
|
* @param {Object} publicKeys
|
||||||
|
* @param {Array<string>} audiences
|
||||||
|
* @param {Object} [options]
|
||||||
|
* @param {Object} [options.expiresSkew=0]
|
||||||
|
* @param {Object} [options.expiresMax=0]
|
||||||
|
* @param {Object} [options.nbfIatSkew=300]
|
||||||
|
* @param {Function<header,body,void>} [options.fixup]
|
||||||
|
*/
|
||||||
|
export default function decode(jwt, publicKeys, audiences, options = defaultOptions) {
|
||||||
|
if (typeof jwt !== 'string') {
|
||||||
|
throw new Error('jwt needs to a string')
|
||||||
|
}
|
||||||
|
|
||||||
|
if (typeof publicKeys !== 'object' || Array.isArray(publicKeys)) {
|
||||||
|
throw new Error(
|
||||||
|
'publicKeys needs to be a map of { issuer: { keyid: "PEM encoded key" }'
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!Array.isArray(audiences)) {
|
||||||
|
throw new Error('audiences needs to be an array of allowed audiences')
|
||||||
|
}
|
||||||
|
|
||||||
|
if (typeof options !== 'object' || Array.isArray(publicKeys)) {
|
||||||
|
throw new Error('options needs to a map of { nbfIatSkew: 300, ... }')
|
||||||
|
}
|
||||||
|
|
||||||
|
let parts = jwt.split(/\./)
|
||||||
|
if (parts.length !== 3) {
|
||||||
|
throw new Error('JWT does not contain 3 dots')
|
||||||
|
}
|
||||||
|
|
||||||
|
let header = JSON.parse(base64UrlSafe.decode(parts[0]).toString('utf8'))
|
||||||
|
let body = JSON.parse(base64UrlSafe.decode(parts[1]).toString('utf8'))
|
||||||
|
if (typeof options.fixup === 'function') {
|
||||||
|
options.fixup(header, body)
|
||||||
|
}
|
||||||
|
|
||||||
|
let hmacAlgo = null
|
||||||
|
switch (header.alg) {
|
||||||
|
case 'HS256':
|
||||||
|
hmacAlgo = 'sha256'
|
||||||
|
break
|
||||||
|
case 'HS384':
|
||||||
|
hmacAlgo = 'sha384'
|
||||||
|
break
|
||||||
|
case 'HS512':
|
||||||
|
hmacAlgo = 'sha512'
|
||||||
|
break
|
||||||
|
default:
|
||||||
|
throw new Error(
|
||||||
|
'Only alg HS256, HS384 and HS512 are supported'
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!body.iss) {
|
||||||
|
throw new Error('No issuer set')
|
||||||
|
}
|
||||||
|
|
||||||
|
let issuer = publicKeys[body.iss]
|
||||||
|
if (!issuer) {
|
||||||
|
throw new Error(`Unknown issuer '${body.iss}'`)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Find public key
|
||||||
|
let pubkeyOrSharedKey =
|
||||||
|
typeof header.kid === 'string'
|
||||||
|
? issuer[`${header.kid}@${header.alg}`]
|
||||||
|
: issuer[`default@${header.alg}`]
|
||||||
|
let overrideOptions = {}
|
||||||
|
|
||||||
|
if (!pubkeyOrSharedKey) {
|
||||||
|
throw new Error(
|
||||||
|
`Unknown pubkey id '${header.kid}' for this issuer`
|
||||||
|
)
|
||||||
|
} else if ((typeof(pubkeyOrSharedKey) !== 'object' || Array.isArray(pubkeyOrSharedKey)) && typeof(pubkeyOrSharedKey) !== 'string') {
|
||||||
|
throw new Error(
|
||||||
|
`Pubkey of '${header.kid || 'default'}' for '${header.alg}' for this issuer is misconfigured`
|
||||||
|
)
|
||||||
|
}
|
||||||
|
if (typeof(pubkeyOrSharedKey) === 'object') {
|
||||||
|
if (typeof(pubkeyOrSharedKey.key) !== 'string') {
|
||||||
|
throw new Error(
|
||||||
|
`Pubkey of '${header.kid || 'default'}' for '${header.alg}' for this issuer is misconfigured`
|
||||||
|
)
|
||||||
|
}
|
||||||
|
overrideOptions = pubkeyOrSharedKey
|
||||||
|
pubkeyOrSharedKey = pubkeyOrSharedKey.key
|
||||||
|
}
|
||||||
|
|
||||||
|
let signatureOrHash = base64UrlSafe.decode(parts[2])
|
||||||
|
const hmac = crypto.createHmac(hmacAlgo, pubkeyOrSharedKey)
|
||||||
|
hmac.update(`${parts[0]}.${parts[1]}`, 'utf8')
|
||||||
|
let signatureBuffer = hmac.digest()
|
||||||
|
|
||||||
|
if (signatureOrHash.length !== signatureBuffer.length || !crypto.timingSafeEqual(signatureOrHash, signatureBuffer)) {
|
||||||
|
throw new Error(`Verification failed with alg '${header.alg}'`)
|
||||||
|
}
|
||||||
|
|
||||||
|
let unixNow = Math.floor(Date.now() / 1000)
|
||||||
|
|
||||||
|
let validators = defaults(options.validators, {
|
||||||
|
aud: validateAudience,
|
||||||
|
exp: validateExpires,
|
||||||
|
iat: validateIssuedAt,
|
||||||
|
nbf: validateNotBefore
|
||||||
|
})
|
||||||
|
|
||||||
|
let validationOptions = defaults(overrideOptions, options)
|
||||||
|
|
||||||
|
validators.aud(body, audiences, validationOptions)
|
||||||
|
validators.iat(body, unixNow, validationOptions)
|
||||||
|
validators.nbf(body, unixNow, validationOptions)
|
||||||
|
validators.exp(body, unixNow, validationOptions)
|
||||||
|
|
||||||
|
return body
|
||||||
|
}
|
||||||
|
|
||||||
|
function validateNotBefore(body, unixNow, options) {
|
||||||
|
if (body.nbf && body.nbf > unixNow + options.nbfIatSkew) {
|
||||||
|
throw new Error(
|
||||||
|
`Not before in the future by more than ${options.nbfIatSkew} seconds`
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function validateIssuedAt(body, unixNow, options) {
|
||||||
|
if (body.iat && body.iat > unixNow + options.nbfIatSkew) {
|
||||||
|
throw new Error(
|
||||||
|
`Issued at in the future by more than ${options.nbfIatSkew} seconds`
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function validateAudience(body, audiences, options) {
|
||||||
|
let auds = Array.isArray(body.aud) ? body.aud : [body.aud]
|
||||||
|
if (!auds.some(aud => audiences.includes(aud))) {
|
||||||
|
throw new Error(`Unknown audience '${auds.join(',')}'`)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function validateExpires(body, unixNow, options) {
|
||||||
|
if (!body.exp) {
|
||||||
|
throw new Error(`No expires set on token`)
|
||||||
|
}
|
||||||
|
let notBefore = body.iat || body.nbf || unixNow
|
||||||
|
if (options.expiresMax && body.exp > notBefore + options.expiresMax) {
|
||||||
|
throw new Error(
|
||||||
|
`Expires in the future by more than ${options.expiresMax} seconds`
|
||||||
|
)
|
||||||
|
}
|
||||||
|
if (body.exp + (options.expiresSkew || 0) <= unixNow) {
|
||||||
|
throw new Error('Token has expired')
|
||||||
|
}
|
||||||
|
}
|
43
api/jwt/encode.mjs
Normal file
43
api/jwt/encode.mjs
Normal file
|
@ -0,0 +1,43 @@
|
||||||
|
import crypto from 'crypto'
|
||||||
|
import * as base64UrlSafe from './base64urlsafe.mjs'
|
||||||
|
|
||||||
|
export default function encode(header, body, privateKeyPassword = null) {
|
||||||
|
if (
|
||||||
|
typeof header !== 'object' ||
|
||||||
|
Array.isArray(header) ||
|
||||||
|
typeof body !== 'object' ||
|
||||||
|
Array.isArray(body)
|
||||||
|
) {
|
||||||
|
throw new Error('both header and body should be of type object')
|
||||||
|
}
|
||||||
|
|
||||||
|
let hmacAlgo = null
|
||||||
|
switch (header.alg) {
|
||||||
|
case 'HS256':
|
||||||
|
hmacAlgo = 'sha256'
|
||||||
|
break
|
||||||
|
case 'HS384':
|
||||||
|
hmacAlgo = 'sha384'
|
||||||
|
break
|
||||||
|
case 'HS512':
|
||||||
|
hmacAlgo = 'sha512'
|
||||||
|
break
|
||||||
|
default:
|
||||||
|
throw new Error(
|
||||||
|
'Only alg HS256, HS384 and HS512 are supported'
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Base64 encode header and body
|
||||||
|
let headerBase64 = base64UrlSafe.encode(Buffer.from(JSON.stringify(header)))
|
||||||
|
let bodyBase64 = base64UrlSafe.encode(Buffer.from(JSON.stringify(body)))
|
||||||
|
let headerBodyBase64 = headerBase64 + '.' + bodyBase64
|
||||||
|
|
||||||
|
const hmac = crypto.createHmac(hmacAlgo, privateKeyPassword)
|
||||||
|
hmac.update(headerBodyBase64)
|
||||||
|
let signatureBuffer = hmac.digest()
|
||||||
|
|
||||||
|
// Construct final JWT
|
||||||
|
let signatureBase64 = base64UrlSafe.encode(signatureBuffer)
|
||||||
|
return headerBodyBase64 + '.' + signatureBase64
|
||||||
|
}
|
29
api/log.mjs
Normal file
29
api/log.mjs
Normal file
|
@ -0,0 +1,29 @@
|
||||||
|
import bunyan from 'bunyan-lite'
|
||||||
|
import config from './config.mjs'
|
||||||
|
import * as defaults from './defaults.mjs'
|
||||||
|
|
||||||
|
|
||||||
|
// Clone the settings as we will be touching
|
||||||
|
// on them slightly.
|
||||||
|
let settings = defaults.default(config.get('bunyan'))
|
||||||
|
|
||||||
|
// Replace any instance of 'process.stdout' with the
|
||||||
|
// actual reference to the process.stdout.
|
||||||
|
for (let i = 0; i < settings.streams.length; i++) {
|
||||||
|
/* istanbul ignore else */
|
||||||
|
if (settings.streams[i].stream === 'process.stdout') {
|
||||||
|
settings.streams[i].stream = process.stdout
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create our logger.
|
||||||
|
const log = bunyan.createLogger(settings)
|
||||||
|
|
||||||
|
export default log
|
||||||
|
|
||||||
|
log.logMiddleware = () =>
|
||||||
|
(ctx, next) => {
|
||||||
|
ctx.log = log
|
||||||
|
|
||||||
|
return next()
|
||||||
|
}
|
|
@ -1,7 +1,6 @@
|
||||||
const fs = require('fs')
|
|
||||||
import multer from 'multer'
|
import multer from 'multer'
|
||||||
|
|
||||||
const storage = multer.diskStorage({
|
/*const storage = multer.diskStorage({
|
||||||
destination: function (req, file, cb) {
|
destination: function (req, file, cb) {
|
||||||
cb(null, '/tmp/my-uploads')
|
cb(null, '/tmp/my-uploads')
|
||||||
},
|
},
|
||||||
|
@ -9,7 +8,7 @@ const storage = multer.diskStorage({
|
||||||
console.log(file)
|
console.log(file)
|
||||||
cb(null, file.fieldname + '-' + Date.now())
|
cb(null, file.fieldname + '-' + Date.now())
|
||||||
}
|
}
|
||||||
})
|
})*/
|
||||||
|
|
||||||
export function uploadFile(ctx, siteName) {
|
export function uploadFile(ctx, siteName) {
|
||||||
return new Promise((res, rej) => {
|
return new Promise((res, rej) => {
|
|
@ -1,6 +1,5 @@
|
||||||
import config from '../../config'
|
import { verifyToken } from './security.mjs'
|
||||||
import { verifyToken } from './security'
|
import { uploadFile } from './multer.mjs'
|
||||||
import { uploadFile, rename } from './multer'
|
|
||||||
|
|
||||||
export async function upload(ctx) {
|
export async function upload(ctx) {
|
||||||
let site = await verifyToken(ctx)
|
let site = await verifyToken(ctx)
|
|
@ -1,12 +1,12 @@
|
||||||
import * as jwt from '../jwt'
|
// import * as jwt from '../jwt.mjs'
|
||||||
import config from '../../config'
|
import config from '../config.mjs'
|
||||||
|
|
||||||
export async function verifyToken(ctx) {
|
export async function verifyToken(ctx) {
|
||||||
if (!ctx.query.token) {
|
if (!ctx.query.token) {
|
||||||
throw new Error('Token is missing in query')
|
throw new Error('Token is missing in query')
|
||||||
}
|
}
|
||||||
|
|
||||||
let decoded = jwt.decode(ctx.query.token)
|
/*let decoded = jwt.decode(ctx.query.token)
|
||||||
|
|
||||||
if (!decoded || !decoded.site) {
|
if (!decoded || !decoded.site) {
|
||||||
throw new Error('Token is invalid')
|
throw new Error('Token is invalid')
|
||||||
|
@ -15,7 +15,7 @@ export async function verifyToken(ctx) {
|
||||||
let output = await jwt.verify(
|
let output = await jwt.verify(
|
||||||
ctx.query.token,
|
ctx.query.token,
|
||||||
config.get(`sites:${decoded.site}`)
|
config.get(`sites:${decoded.site}`)
|
||||||
)
|
)*/
|
||||||
|
|
||||||
return output.site
|
return output.site
|
||||||
}
|
}
|
|
@ -1,11 +1,12 @@
|
||||||
import Router from 'koa-router'
|
import Router from 'koa-router'
|
||||||
|
|
||||||
import * as test from './test/routes'
|
import * as test from './test/routes.mjs'
|
||||||
import * as media from './media/routes'
|
import * as media from './media/routes.mjs'
|
||||||
|
|
||||||
const router = new Router()
|
const router = new Router()
|
||||||
|
|
||||||
router.get('/', test.testStatic)
|
router.get('/', test.testStatic)
|
||||||
|
router.get('/error', test.testError)
|
||||||
router.post('/media', media.upload)
|
router.post('/media', media.upload)
|
||||||
|
|
||||||
export default router
|
export default router
|
18
api/server.mjs
Normal file
18
api/server.mjs
Normal file
|
@ -0,0 +1,18 @@
|
||||||
|
import Koa from 'koa-lite'
|
||||||
|
|
||||||
|
import config from './config.mjs'
|
||||||
|
import log from './log.mjs'
|
||||||
|
import router from './router.mjs'
|
||||||
|
import { errorMiddleware } from './error.mjs'
|
||||||
|
|
||||||
|
const app = new Koa()
|
||||||
|
|
||||||
|
app.use(errorMiddleware)
|
||||||
|
app.use(router.routes())
|
||||||
|
app.use(router.allowedMethods())
|
||||||
|
|
||||||
|
const server = app.listen(config.get('server:port'), function(a,b) {
|
||||||
|
log.info(`Server listening at ${config.get('server:port')}`)
|
||||||
|
})
|
||||||
|
|
||||||
|
export default server
|
|
@ -1,4 +1,4 @@
|
||||||
import config from '../../config'
|
import config from '../config.mjs'
|
||||||
|
|
||||||
export async function testStatic(ctx) {
|
export async function testStatic(ctx) {
|
||||||
ctx.body = {
|
ctx.body = {
|
||||||
|
@ -7,3 +7,7 @@ export async function testStatic(ctx) {
|
||||||
environment: config.get('NODE_ENV'),
|
environment: config.get('NODE_ENV'),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function testError(ctx) {
|
||||||
|
throw new Error('This is a test')
|
||||||
|
}
|
3
index.js
3
index.js
|
@ -1,3 +0,0 @@
|
||||||
require('babel-register')
|
|
||||||
|
|
||||||
require('./server')
|
|
26
package.json
26
package.json
|
@ -5,13 +5,8 @@
|
||||||
"main": "index.js",
|
"main": "index.js",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "nodemon index.js",
|
"dev": "nodemon index.js",
|
||||||
"start": "node index.js",
|
"start": "node --experimental-modules api/server.mjs",
|
||||||
"test": "env NODE_ENV=test mocha --require babel-register --recursive --reporter dot",
|
"test": "eltro test/**/*.test.mjs -r dot"
|
||||||
"docker": "docker run -it --rm --name my-running-script -v \"$PWD\":/usr/src/app -w /usr/src/app node:alpine",
|
|
||||||
"install:test": "npm install --no-optional && npm run test",
|
|
||||||
"install:dev": "npm install --no-optional && npm run dev",
|
|
||||||
"docker:test": "npm run docker -- npm run install:test",
|
|
||||||
"docker:dev": "npm run docker -- npm run install:dev"
|
|
||||||
},
|
},
|
||||||
"repository": {
|
"repository": {
|
||||||
"type": "git",
|
"type": "git",
|
||||||
|
@ -24,20 +19,13 @@
|
||||||
},
|
},
|
||||||
"homepage": "https://github.com/nfp-projects/storage-upload#readme",
|
"homepage": "https://github.com/nfp-projects/storage-upload#readme",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"babel-plugin-transform-es2015-modules-commonjs": "^6.26.0",
|
"bunyan-lite": "^1.1.1",
|
||||||
"babel-register": "^6.26.0",
|
"koa-lite": "^2.10.1",
|
||||||
"koa": "^2.3.0",
|
|
||||||
"multer": "^1.3.0",
|
|
||||||
"koa-router": "^7.2.1",
|
"koa-router": "^7.2.1",
|
||||||
"jsonwebtoken": "^8.1.0",
|
"multer": "^1.3.0",
|
||||||
"nconf": "^0.8.5"
|
"nconf-lite": "^2.0.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"app-root-path": "^2.0.1",
|
"eltro": "^1.1.0"
|
||||||
"assert-extended": "^1.0.1",
|
|
||||||
"mocha": "^4.0.1",
|
|
||||||
"nodemon": "^1.12.1",
|
|
||||||
"request-json": "^0.6.2",
|
|
||||||
"sinon": "^4.1.3"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
15
server.js
15
server.js
|
@ -1,15 +0,0 @@
|
||||||
import Koa from 'koa'
|
|
||||||
|
|
||||||
import config from './config'
|
|
||||||
import router from './api/router'
|
|
||||||
import { errorMiddleware } from './api/error'
|
|
||||||
|
|
||||||
const app = new Koa()
|
|
||||||
|
|
||||||
app.use(errorMiddleware)
|
|
||||||
app.use(router.routes())
|
|
||||||
app.use(router.allowedMethods())
|
|
||||||
|
|
||||||
const server = app.listen(config.get('server:port'))
|
|
||||||
|
|
||||||
export default server
|
|
|
@ -1,32 +0,0 @@
|
||||||
import assert from 'assert-extended'
|
|
||||||
import sinon from 'sinon'
|
|
||||||
|
|
||||||
describe('defaults', () => {
|
|
||||||
const defaults = require('../api/defaults').default
|
|
||||||
|
|
||||||
describe('#defaults()', () => {
|
|
||||||
it('should apply defaults to flat objects', () => {
|
|
||||||
let assertOutput = { a: 1 }
|
|
||||||
let output = defaults(null, { a: 1 })
|
|
||||||
|
|
||||||
assert.deepEqual(output, assertOutput)
|
|
||||||
})
|
|
||||||
|
|
||||||
it('should allow overriding defult properties', () => {
|
|
||||||
let assertOutput = { a: 2 }
|
|
||||||
let output = defaults(assertOutput, { a: 1 })
|
|
||||||
|
|
||||||
assert.deepEqual(output, assertOutput)
|
|
||||||
})
|
|
||||||
|
|
||||||
it('should allow nesting through objects', () => {
|
|
||||||
let def = { a: { b: 2 } }
|
|
||||||
let output = defaults({ a: { c: 3} }, def)
|
|
||||||
|
|
||||||
assert.deepEqual(output.a, {
|
|
||||||
b: 2,
|
|
||||||
c: 3,
|
|
||||||
})
|
|
||||||
})
|
|
||||||
})
|
|
||||||
})
|
|
31
test/defaults.test.mjs
Normal file
31
test/defaults.test.mjs
Normal file
|
@ -0,0 +1,31 @@
|
||||||
|
import { Eltro as t, assert} from 'eltro'
|
||||||
|
import defaults from '../api/defaults.mjs'
|
||||||
|
|
||||||
|
t.describe('#defaults()', () => {
|
||||||
|
t.test('should apply defaults to flat objects', () => {
|
||||||
|
let assertOutput = { a: 1 }
|
||||||
|
let output = defaults(null, { a: 1 })
|
||||||
|
|
||||||
|
assert.deepStrictEqual(output, assertOutput)
|
||||||
|
output = defaults({ a: 1 })
|
||||||
|
|
||||||
|
assert.deepStrictEqual(output, assertOutput)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.test('should allow overriding defult properties', () => {
|
||||||
|
let assertOutput = { a: 2 }
|
||||||
|
let output = defaults(assertOutput, { a: 1 })
|
||||||
|
|
||||||
|
assert.deepStrictEqual(output, assertOutput)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.test('should allow nesting through objects', () => {
|
||||||
|
let def = { a: { b: 2 } }
|
||||||
|
let output = defaults({ a: { c: 3} }, def)
|
||||||
|
|
||||||
|
assert.deepStrictEqual(output.a, {
|
||||||
|
b: 2,
|
||||||
|
c: 3,
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
|
@ -1,54 +0,0 @@
|
||||||
import assert from 'assert-extended'
|
|
||||||
import sinon from 'sinon'
|
|
||||||
|
|
||||||
import { createContext } from './helper.server'
|
|
||||||
|
|
||||||
describe('Error (Middleware)', () => {
|
|
||||||
const error = require('../api/error')
|
|
||||||
|
|
||||||
let ctx
|
|
||||||
|
|
||||||
beforeEach(() => {
|
|
||||||
ctx = createContext({ })
|
|
||||||
})
|
|
||||||
|
|
||||||
describe('#errorMiddleware()', () => {
|
|
||||||
let stub
|
|
||||||
|
|
||||||
beforeEach(() => {
|
|
||||||
stub = sinon.stub()
|
|
||||||
})
|
|
||||||
|
|
||||||
it('should call next and not do anything if success', async () => {
|
|
||||||
await error.errorMiddleware(ctx, stub)
|
|
||||||
|
|
||||||
assert.ok(stub.called)
|
|
||||||
assert.strictEqual(ctx.body, undefined)
|
|
||||||
assert.strictEqual(ctx.status, undefined)
|
|
||||||
})
|
|
||||||
|
|
||||||
it('should support stub throwing', async () => {
|
|
||||||
let assertError = new Error('testetytest')
|
|
||||||
stub.throws(assertError)
|
|
||||||
|
|
||||||
await error.errorMiddleware(ctx, stub)
|
|
||||||
|
|
||||||
assert.ok(ctx.body)
|
|
||||||
assert.strictEqual(ctx.status, 422)
|
|
||||||
assert.strictEqual(ctx.body.status, 422)
|
|
||||||
assert.strictEqual(ctx.body.message, assertError.message)
|
|
||||||
})
|
|
||||||
|
|
||||||
it('should support stub resolving false', async () => {
|
|
||||||
let assertError = new Error('herpaderpderp')
|
|
||||||
stub.rejects(assertError)
|
|
||||||
|
|
||||||
await error.errorMiddleware(ctx, stub)
|
|
||||||
|
|
||||||
assert.ok(ctx.body)
|
|
||||||
assert.strictEqual(ctx.status, 422)
|
|
||||||
assert.strictEqual(ctx.body.status, 422)
|
|
||||||
assert.strictEqual(ctx.body.message, assertError.message)
|
|
||||||
})
|
|
||||||
})
|
|
||||||
})
|
|
|
@ -1,89 +0,0 @@
|
||||||
import request from 'request-json'
|
|
||||||
|
|
||||||
import defaults from '../api/defaults'
|
|
||||||
import config from '../config'
|
|
||||||
|
|
||||||
function parseBody(body, reject) {
|
|
||||||
try {
|
|
||||||
return JSON.parse(body)
|
|
||||||
} catch (error) {
|
|
||||||
// eslint-disable-next-line no-console
|
|
||||||
console.log(body)
|
|
||||||
return reject(error)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function callback(resolve, reject) {
|
|
||||||
return (err, res, rawBody) => {
|
|
||||||
let body = rawBody
|
|
||||||
if (err) {
|
|
||||||
return reject(err)
|
|
||||||
}
|
|
||||||
if (typeof body === 'string' && body) {
|
|
||||||
body = parseBody(body, reject)
|
|
||||||
}
|
|
||||||
if (res.statusCode >= 300 ||
|
|
||||||
res.statusCode < 200) {
|
|
||||||
return reject(body)
|
|
||||||
}
|
|
||||||
resolve(body)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export default function createClient(host = config.get('server:port'), opts) {
|
|
||||||
let options = defaults(opts, {})
|
|
||||||
|
|
||||||
let client = request.createClient('', options)
|
|
||||||
let prefix
|
|
||||||
|
|
||||||
prefix = `http://localhost:${host}`
|
|
||||||
client.headers['x-request-id'] = 'asdf'
|
|
||||||
|
|
||||||
client.auth = (user) => {
|
|
||||||
// let m = helperDB.model('user', {
|
|
||||||
// id: user.id,
|
|
||||||
// level: (user.get && user.get('level')) || 1,
|
|
||||||
// institute_id: (user.get && user.get('institute_id')) || null,
|
|
||||||
// password: (user.get && user.get('password')) || null,
|
|
||||||
// })
|
|
||||||
// let token = jwt.createUserToken(m)
|
|
||||||
// client.headers.authorization = `Bearer ${token}`
|
|
||||||
}
|
|
||||||
|
|
||||||
// Simple wrappers to wrap into promises
|
|
||||||
client.getAsync = (path) =>
|
|
||||||
new Promise((resolve, reject) => {
|
|
||||||
if (path.slice(0, 4) === 'http') {
|
|
||||||
return client.get(path, callback(resolve, reject))
|
|
||||||
}
|
|
||||||
client.get(prefix + path, callback(resolve, reject))
|
|
||||||
})
|
|
||||||
|
|
||||||
// Simple wrappers to wrap into promises
|
|
||||||
client.saveFileAsync = (path, destination) =>
|
|
||||||
new Promise((resolve, reject) => {
|
|
||||||
client.saveFile(prefix + path, destination, callback(resolve, reject, true))
|
|
||||||
})
|
|
||||||
|
|
||||||
client.postAsync = (path, data) =>
|
|
||||||
new Promise((resolve, reject) => {
|
|
||||||
client.post(prefix + path, data, callback(resolve, reject))
|
|
||||||
})
|
|
||||||
|
|
||||||
client.putAsync = (path, data) =>
|
|
||||||
new Promise((resolve, reject) => {
|
|
||||||
client.put(prefix + path, data, callback(resolve, reject))
|
|
||||||
})
|
|
||||||
|
|
||||||
client.deleteAsync = (path) =>
|
|
||||||
new Promise((resolve, reject) => {
|
|
||||||
client.del(prefix + path, callback(resolve, reject))
|
|
||||||
})
|
|
||||||
|
|
||||||
client.sendFileAsync = (path, files, data) =>
|
|
||||||
new Promise((resolve, reject) => {
|
|
||||||
client.sendFile(prefix + path, files, data || {}, callback(resolve, reject))
|
|
||||||
})
|
|
||||||
|
|
||||||
return client
|
|
||||||
}
|
|
123
test/helper.client.mjs
Normal file
123
test/helper.client.mjs
Normal file
|
@ -0,0 +1,123 @@
|
||||||
|
import http from 'http'
|
||||||
|
import { URL } from 'url'
|
||||||
|
import defaults from '../api/defaults.mjs'
|
||||||
|
import config from '../api/config.mjs'
|
||||||
|
|
||||||
|
export default function Client(port = config.get('server:port'), opts) {
|
||||||
|
this.options = defaults(opts, {})
|
||||||
|
this.prefix = `http://localhost:${port}`
|
||||||
|
}
|
||||||
|
|
||||||
|
Client.prototype.customRequest = function(method = 'GET', path, body, options) {
|
||||||
|
if (path.slice(0, 4) !== 'http') {
|
||||||
|
path = this.prefix + path
|
||||||
|
}
|
||||||
|
let urlObj = new URL(path)
|
||||||
|
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
const opts = defaults(defaults(options, {
|
||||||
|
method: method,
|
||||||
|
timeout: 500,
|
||||||
|
protocol: urlObj.protocol,
|
||||||
|
username: urlObj.username,
|
||||||
|
password: urlObj.password,
|
||||||
|
host: urlObj.hostname,
|
||||||
|
port: Number(urlObj.port),
|
||||||
|
path: urlObj.pathname + urlObj.search,
|
||||||
|
}))
|
||||||
|
|
||||||
|
const req = http.request(opts)
|
||||||
|
if (body) {
|
||||||
|
req.write(body)
|
||||||
|
}
|
||||||
|
|
||||||
|
req.on('error', reject)
|
||||||
|
req.on('timeout', function() { reject(new Error(`Request ${method} ${path} timed out`)) })
|
||||||
|
req.on('response', res => {
|
||||||
|
res.setEncoding('utf8')
|
||||||
|
let output = ''
|
||||||
|
|
||||||
|
res.on('data', function (chunk) {
|
||||||
|
output += chunk.toString()
|
||||||
|
})
|
||||||
|
|
||||||
|
res.on('end', function () {
|
||||||
|
try {
|
||||||
|
output = JSON.parse(output)
|
||||||
|
} catch (e) {
|
||||||
|
return reject(new Error(`${e.message} while decoding: ${output}`))
|
||||||
|
}
|
||||||
|
if (output.status) {
|
||||||
|
let err = new Error(`Request failed [${output.status}]: ${output.message}`)
|
||||||
|
err.body = output
|
||||||
|
return reject(err)
|
||||||
|
}
|
||||||
|
resolve(output)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
req.end()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
Client.prototype.get = function(path = '/') {
|
||||||
|
return this.customRequest('GET', path, null)
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
|
||||||
|
export function createClient(host = config.get('server:port'), opts) {
|
||||||
|
let options = defaults(opts, {})
|
||||||
|
|
||||||
|
let prefix = `http://localhost:${host}`
|
||||||
|
options.headers['x-request-id'] = 'asdf'
|
||||||
|
|
||||||
|
client.auth = (user) => {
|
||||||
|
// let m = helperDB.model('user', {
|
||||||
|
// id: user.id,
|
||||||
|
// level: (user.get && user.get('level')) || 1,
|
||||||
|
// institute_id: (user.get && user.get('institute_id')) || null,
|
||||||
|
// password: (user.get && user.get('password')) || null,
|
||||||
|
// })
|
||||||
|
// let token = jwt.createUserToken(m)
|
||||||
|
// client.headers.authorization = `Bearer ${token}`
|
||||||
|
}
|
||||||
|
|
||||||
|
// Simple wrappers to wrap into promises
|
||||||
|
client.getAsync = (path) =>
|
||||||
|
new Promise((resolve, reject) => {
|
||||||
|
if (path.slice(0, 4) === 'http') {
|
||||||
|
return client.get(path, callback(resolve, reject))
|
||||||
|
}
|
||||||
|
client.get(prefix + path, callback(resolve, reject))
|
||||||
|
})
|
||||||
|
|
||||||
|
// Simple wrappers to wrap into promises
|
||||||
|
client.saveFileAsync = (path, destination) =>
|
||||||
|
new Promise((resolve, reject) => {
|
||||||
|
client.saveFile(prefix + path, destination, callback(resolve, reject, true))
|
||||||
|
})
|
||||||
|
|
||||||
|
client.postAsync = (path, data) =>
|
||||||
|
new Promise((resolve, reject) => {
|
||||||
|
client.post(prefix + path, data, callback(resolve, reject))
|
||||||
|
})
|
||||||
|
|
||||||
|
client.putAsync = (path, data) =>
|
||||||
|
new Promise((resolve, reject) => {
|
||||||
|
client.put(prefix + path, data, callback(resolve, reject))
|
||||||
|
})
|
||||||
|
|
||||||
|
client.deleteAsync = (path) =>
|
||||||
|
new Promise((resolve, reject) => {
|
||||||
|
client.del(prefix + path, callback(resolve, reject))
|
||||||
|
})
|
||||||
|
|
||||||
|
client.sendFileAsync = (path, files, data) =>
|
||||||
|
new Promise((resolve, reject) => {
|
||||||
|
client.sendFile(prefix + path, files, data || {}, callback(resolve, reject))
|
||||||
|
})
|
||||||
|
|
||||||
|
return client
|
||||||
|
}
|
||||||
|
|
||||||
|
*/
|
|
@ -1,12 +1,12 @@
|
||||||
// import _ from 'lodash'
|
// import _ from 'lodash'
|
||||||
// import sinon from 'sinon'
|
// import sinon from 'sinon'
|
||||||
import server from '../server'
|
import Client from './helper.client.mjs'
|
||||||
import client from './helper.client'
|
import defaults from '../api/defaults.mjs'
|
||||||
import defaults from '../api/defaults'
|
import '../api/server.mjs'
|
||||||
|
|
||||||
after(() => server.close())
|
export function createClient() {
|
||||||
|
return new Client()
|
||||||
export const createClient = client
|
}
|
||||||
|
|
||||||
export function createContext(opts) {
|
export function createContext(opts) {
|
||||||
return defaults(opts, {
|
return defaults(opts, {
|
|
@ -1,61 +0,0 @@
|
||||||
import assert from 'assert-extended'
|
|
||||||
import sinon from 'sinon'
|
|
||||||
|
|
||||||
describe('jwt', () => {
|
|
||||||
const jsonwebtoken = require('jsonwebtoken')
|
|
||||||
const jwt = require('../api/jwt')
|
|
||||||
|
|
||||||
describe('#sign', () => {
|
|
||||||
it('should call security correctly', () => {
|
|
||||||
let token = jwt.sign({ a: 1 }, 'asdf')
|
|
||||||
assert.ok(token)
|
|
||||||
|
|
||||||
let decoded = jsonwebtoken.decode(token)
|
|
||||||
assert.strictEqual(decoded.a, 1)
|
|
||||||
})
|
|
||||||
|
|
||||||
it('should support custom secret', done => {
|
|
||||||
const assertSecret = 'sdfagsda'
|
|
||||||
let token = jwt.sign({ a: 1 }, assertSecret)
|
|
||||||
|
|
||||||
jsonwebtoken.verify(token, assertSecret, done)
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
describe('#decode()', () => {
|
|
||||||
it('should decode correctly', () => {
|
|
||||||
let data = { a: 1, b: 2 }
|
|
||||||
let token = jwt.sign(data, 'asdf')
|
|
||||||
let decoded = jwt.decode(token)
|
|
||||||
|
|
||||||
assert.strictEqual(decoded.a, data.a)
|
|
||||||
assert.strictEqual(decoded.b, data.b)
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
describe('#verify', () => {
|
|
||||||
it('should verify correctly', () => {
|
|
||||||
const assertSecret = 'asdfasdf'
|
|
||||||
const assertResult = 23532
|
|
||||||
let token = jwt.sign({ a: assertResult }, assertSecret)
|
|
||||||
|
|
||||||
return assert.isFulfilled(jwt.verify(token, assertSecret))
|
|
||||||
.then(data => assert.strictEqual(data.a, assertResult))
|
|
||||||
})
|
|
||||||
|
|
||||||
it('should fail if secret does not match', () => {
|
|
||||||
const assertSecret = 'asdfasdf'
|
|
||||||
let token = jwt.sign({ a: 1 }, assertSecret)
|
|
||||||
|
|
||||||
return assert.isRejected(jwt.verify(token, assertSecret + 'a'))
|
|
||||||
.then(err => assert.match(err.message, /[Ss]ignature/))
|
|
||||||
})
|
|
||||||
|
|
||||||
it('should fail token has been mishandled', () => {
|
|
||||||
let token = jwt.sign({ a: 1 }, 'asdf')
|
|
||||||
|
|
||||||
return assert.isRejected(jwt.verify(token + 'a', 'asdf'))
|
|
||||||
.then(err => assert.match(err.message, /[Ss]ignature/))
|
|
||||||
})
|
|
||||||
})
|
|
||||||
})
|
|
72
test/jwt/decode.test.mjs
Normal file
72
test/jwt/decode.test.mjs
Normal file
|
@ -0,0 +1,72 @@
|
||||||
|
import { Eltro as t, assert} from 'eltro'
|
||||||
|
import decode from '../../api/jwt/decode.mjs'
|
||||||
|
|
||||||
|
const pubKeys = {}
|
||||||
|
const audiences = []
|
||||||
|
|
||||||
|
const testJwt =
|
||||||
|
'eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiIsImtpZCI6IjEifQ.eyJhdWQiOiJodHRwczovL2hvc3Qvb2F1dGgvdG9rZW4iLCJpc3MiOiJ0ZXN0QHRlc3QuY29tIiwiaWF0IjoxNTAzMzM1MTY5LCJleHAiOjE1MDMzMzU3NjksInNjb3BlIjpbImh0dHA6Ly9zdHVmZiIsImh0dHA6Ly9zdHVmZjIiXX0.zO278VV6NzwsvBrAIc15mOfwza-FkmLCV28NRXnrI550xw1S1145cS1UsZP5zXxcrk5f4oEgB91Jt6ble76yK5nU68fALUXtfe7xPUkhcOUIw92q_x_Iaaw4z6a71NtyishCfJlbmwkXXEq5YCVAvX3KNDtyPf_fQrAqjzsbgQc'
|
||||||
|
|
||||||
|
const testJwtWrongAlg =
|
||||||
|
'eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzEyOCJ9.eyJhdWQiOiJodHRwczovL2hvc3Qvb2F1dGgvdG9rZW4iLCJpc3MiOiJ0ZXN0QHRlc3QuY29tIiwiaWF0IjoxNTAzMzM2NzU5LCJleHAiOjE1MDMzMzczNTksInNjb3BlIjpbImh0dHA6Ly9zdHVmZiIsImh0dHA6Ly9zdHVmZjIiXX0.12co2gXwBxmZ2uLJecd26bfteCLBx7jgu_9rp2hhKAHWA4qFKm1HcQOZXqDvHkjflQDtNAQ1ZUUf3U8kntUUAmMOjhHx0BspC-xuaTFylZWqj--A2_w9e7JSk46TF_x3e_hZLB3rtyuSEAPMh_nOCsmM-4A2fnQx0Y5p-Bwbt0I'
|
||||||
|
|
||||||
|
t.describe('jwtUtils', function() {
|
||||||
|
t.describe('decode', function() {
|
||||||
|
t.test('invalid jwt input', function() {
|
||||||
|
assert.throws(
|
||||||
|
function() {
|
||||||
|
decode({}, pubKeys, audiences)
|
||||||
|
},
|
||||||
|
/jwt needs to a string/
|
||||||
|
)
|
||||||
|
})
|
||||||
|
t.test('invalid pubKeys input', function() {
|
||||||
|
assert.throws(
|
||||||
|
function() {
|
||||||
|
decode(testJwt, [], audiences)
|
||||||
|
},
|
||||||
|
/publicKeys needs to be a map of { issuer: { keyid: "PEM encoded key" }/
|
||||||
|
)
|
||||||
|
})
|
||||||
|
t.test('invalid audiences input', function() {
|
||||||
|
assert.throws(
|
||||||
|
function() {
|
||||||
|
decode(testJwt, pubKeys, '')
|
||||||
|
},
|
||||||
|
/audiences needs to be an array of allowed audiences/
|
||||||
|
)
|
||||||
|
})
|
||||||
|
t.test('invalid options input', function() {
|
||||||
|
assert.throws(
|
||||||
|
function() {
|
||||||
|
decode(testJwt, pubKeys, audiences, '')
|
||||||
|
},
|
||||||
|
/options needs to a map of { nbfIatSkew: 300, ... }/
|
||||||
|
)
|
||||||
|
})
|
||||||
|
t.test('too few spaces', function() {
|
||||||
|
assert.throws(
|
||||||
|
function() {
|
||||||
|
decode('hello.test', pubKeys, audiences)
|
||||||
|
},
|
||||||
|
/JWT does not contain 3 dots/
|
||||||
|
)
|
||||||
|
})
|
||||||
|
t.test('invalid json', function() {
|
||||||
|
assert.throws(
|
||||||
|
function() {
|
||||||
|
decode(testJwt.substr(10), pubKeys, audiences)
|
||||||
|
},
|
||||||
|
/Unexpected token \$ in JSON at position 0/
|
||||||
|
)
|
||||||
|
})
|
||||||
|
t.test('wrong alg', function() {
|
||||||
|
assert.throws(
|
||||||
|
function() {
|
||||||
|
decode(testJwtWrongAlg, pubKeys, audiences)
|
||||||
|
},
|
||||||
|
/Only alg HS256, HS384 and HS512 are supported/
|
||||||
|
)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
21
test/jwt/encode.test.mjs
Normal file
21
test/jwt/encode.test.mjs
Normal file
|
@ -0,0 +1,21 @@
|
||||||
|
import { Eltro as t, assert} from 'eltro'
|
||||||
|
import encode from '../../api/jwt/encode.mjs'
|
||||||
|
|
||||||
|
t.describe('encode', function() {
|
||||||
|
t.test('should faile with invalid header and body', function() {
|
||||||
|
assert.throws(
|
||||||
|
function() {
|
||||||
|
encode('', '')
|
||||||
|
},
|
||||||
|
/both header and body should be of type object/
|
||||||
|
)
|
||||||
|
})
|
||||||
|
t.test('should faile with empty header and body', function() {
|
||||||
|
assert.throws(
|
||||||
|
function() {
|
||||||
|
encode({}, {})
|
||||||
|
},
|
||||||
|
/Only alg HS256, HS384 and HS512 are supported/
|
||||||
|
)
|
||||||
|
})
|
||||||
|
})
|
343
test/jwt/jwt.test.mjs
Normal file
343
test/jwt/jwt.test.mjs
Normal file
|
@ -0,0 +1,343 @@
|
||||||
|
import { Eltro as t, assert} from 'eltro'
|
||||||
|
import encode from '../../api/jwt/encode.mjs'
|
||||||
|
import decode from '../../api/jwt/decode.mjs'
|
||||||
|
import defaults from '../../api/defaults.mjs'
|
||||||
|
|
||||||
|
const unixNow = Math.floor(Date.now() / 1000)
|
||||||
|
const jwtHeader = {
|
||||||
|
typ: 'JWT',
|
||||||
|
alg: 'HS256',
|
||||||
|
kid: '2'
|
||||||
|
}
|
||||||
|
|
||||||
|
const jwtBody = {
|
||||||
|
aud: 'https://host/oauth/token',
|
||||||
|
iss: 'test@test.com',
|
||||||
|
iat: unixNow,
|
||||||
|
exp: unixNow + 600,
|
||||||
|
scope: ['http://stuff', 'http://stuff2']
|
||||||
|
}
|
||||||
|
|
||||||
|
const pubKeys = {
|
||||||
|
'test@test.com': {
|
||||||
|
'default@HS256': 'sharedkey',
|
||||||
|
'2@HS256': 'sharedkey',
|
||||||
|
'2@HS384': 'sharedkey',
|
||||||
|
'2@HS512': 'sharedkey',
|
||||||
|
'5@HS256': 'wrongkey'
|
||||||
|
},
|
||||||
|
'test@custom.com': {
|
||||||
|
'2@HS256': {
|
||||||
|
key: 'sharedkey',
|
||||||
|
expiresSkew: 600,
|
||||||
|
expiresMax: 86400
|
||||||
|
},
|
||||||
|
'3@HS256': {
|
||||||
|
},
|
||||||
|
'4@HS256': [],
|
||||||
|
'5@HS256': { key: 'sharedkey' },
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
t.describe('encode/decode', function() {
|
||||||
|
['HS256', 'HS384', 'HS512'].forEach(function(algo) {
|
||||||
|
t.test('success with ' + algo, function() {
|
||||||
|
let customJwtHeader = defaults(jwtHeader)
|
||||||
|
customJwtHeader.kid = '2'
|
||||||
|
customJwtHeader.alg = algo
|
||||||
|
let jwt = encode(customJwtHeader, jwtBody, 'sharedkey')
|
||||||
|
let decodedJwtBody = decode(jwt, pubKeys, [
|
||||||
|
'https://host/oauth/token'
|
||||||
|
])
|
||||||
|
assert.deepStrictEqual(jwtBody, decodedJwtBody)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
t.test('success without kid', function() {
|
||||||
|
let customJwtHeader = defaults(jwtHeader)
|
||||||
|
delete customJwtHeader.kid
|
||||||
|
let jwt = encode(customJwtHeader, jwtBody, 'sharedkey')
|
||||||
|
let decodedJwtBody = decode(jwt, pubKeys, [
|
||||||
|
'https://host/oauth/token'
|
||||||
|
])
|
||||||
|
assert.deepStrictEqual(jwtBody, decodedJwtBody)
|
||||||
|
})
|
||||||
|
t.test('success with object key', function() {
|
||||||
|
let customJwtBody = defaults(jwtBody)
|
||||||
|
customJwtBody.kid = '5'
|
||||||
|
customJwtBody.iss = 'test@custom.com'
|
||||||
|
let jwt = encode(jwtHeader, jwtBody, 'sharedkey')
|
||||||
|
let decodedJwtBody = decode(jwt, pubKeys, [
|
||||||
|
'https://host/oauth/token'
|
||||||
|
])
|
||||||
|
assert.deepStrictEqual(jwtBody, decodedJwtBody)
|
||||||
|
})
|
||||||
|
t.test('success with array aud', function() {
|
||||||
|
let customJwtBody = defaults(jwtBody)
|
||||||
|
customJwtBody.aud = [
|
||||||
|
'https://myhost/oauth/token',
|
||||||
|
'https://host/oauth/token'
|
||||||
|
]
|
||||||
|
let jwt = encode(jwtHeader, customJwtBody, 'sharedkey')
|
||||||
|
let decodedJwtBody = decode(jwt, pubKeys, [
|
||||||
|
'https://host/oauth/token'
|
||||||
|
])
|
||||||
|
assert.deepStrictEqual(customJwtBody, decodedJwtBody)
|
||||||
|
})
|
||||||
|
t.test('success with expired token', function() {
|
||||||
|
let customJwtBody = defaults(jwtBody)
|
||||||
|
customJwtBody.iss = 'test@custom.com'
|
||||||
|
customJwtBody.exp -= 600
|
||||||
|
let jwt = encode(jwtHeader, customJwtBody, 'sharedkey')
|
||||||
|
let decodedJwtBody = decode(jwt, pubKeys, [
|
||||||
|
'https://host/oauth/token'
|
||||||
|
])
|
||||||
|
assert.deepStrictEqual(customJwtBody, decodedJwtBody)
|
||||||
|
})
|
||||||
|
t.test('token outside maximum expires', function() {
|
||||||
|
let customJwtBody = defaults(jwtBody)
|
||||||
|
customJwtBody.iss = 'test@custom.com'
|
||||||
|
customJwtBody.exp += 172800
|
||||||
|
let jwt = encode(jwtHeader, customJwtBody, 'sharedkey')
|
||||||
|
assert.throws(
|
||||||
|
function() {
|
||||||
|
decode(jwt, pubKeys, ['https://host/oauth/token'])
|
||||||
|
},
|
||||||
|
/Expires in the future by more than 86400 seconds/
|
||||||
|
)
|
||||||
|
})
|
||||||
|
t.test('token outside maximum expires using decode options', function() {
|
||||||
|
let customJwtBody = defaults(jwtBody)
|
||||||
|
customJwtBody.exp += 172800
|
||||||
|
let jwt = encode(jwtHeader, customJwtBody, 'sharedkey')
|
||||||
|
assert.throws(
|
||||||
|
function() {
|
||||||
|
decode(jwt, pubKeys, ['https://host/oauth/token'], {
|
||||||
|
expiresMax: 600
|
||||||
|
})
|
||||||
|
},
|
||||||
|
/Expires in the future by more than 600 seconds/
|
||||||
|
)
|
||||||
|
})
|
||||||
|
t.test('token outside maximum expires using nbf', function() {
|
||||||
|
let customJwtBody = defaults(jwtBody)
|
||||||
|
customJwtBody.exp += 172800
|
||||||
|
customJwtBody.nbf = customJwtBody.iat
|
||||||
|
delete customJwtBody.iat
|
||||||
|
let jwt = encode(jwtHeader, customJwtBody, 'sharedkey')
|
||||||
|
assert.throws(
|
||||||
|
function() {
|
||||||
|
decode(jwt, pubKeys, ['https://host/oauth/token'], {
|
||||||
|
expiresMax: 600
|
||||||
|
})
|
||||||
|
},
|
||||||
|
/Expires in the future by more than 600 seconds/
|
||||||
|
)
|
||||||
|
})
|
||||||
|
t.test('token outside maximum expires using unixNow', function() {
|
||||||
|
let customJwtBody = defaults(jwtBody)
|
||||||
|
customJwtBody.exp += 172800
|
||||||
|
delete customJwtBody.iat
|
||||||
|
let jwt = encode(jwtHeader, customJwtBody, 'sharedkey')
|
||||||
|
assert.throws(
|
||||||
|
function() {
|
||||||
|
decode(jwt, pubKeys, ['https://host/oauth/token'], {
|
||||||
|
expiresMax: 600
|
||||||
|
})
|
||||||
|
},
|
||||||
|
/Expires in the future by more than 600 seconds/
|
||||||
|
)
|
||||||
|
})
|
||||||
|
t.test('unknown aud', function() {
|
||||||
|
let jwt = encode(jwtHeader, jwtBody, 'sharedkey')
|
||||||
|
assert.throws(
|
||||||
|
function() {
|
||||||
|
decode(jwt, pubKeys, ['https://myhost/oauth/token'])
|
||||||
|
},
|
||||||
|
/Unknown audience 'https:\/\/host\/oauth\/token'/
|
||||||
|
)
|
||||||
|
})
|
||||||
|
t.test('expired', function() {
|
||||||
|
let customJwtBody = defaults(jwtBody)
|
||||||
|
customJwtBody.iat -= 1200
|
||||||
|
customJwtBody.exp -= 800
|
||||||
|
let jwt = encode(jwtHeader, customJwtBody, 'sharedkey')
|
||||||
|
assert.throws(
|
||||||
|
function() {
|
||||||
|
decode(jwt, pubKeys, ['https://host/oauth/token'])
|
||||||
|
},
|
||||||
|
/Token has expired/
|
||||||
|
)
|
||||||
|
})
|
||||||
|
t.test('missing exp', function() {
|
||||||
|
let customJwtBody = defaults(jwtBody)
|
||||||
|
delete customJwtBody.exp
|
||||||
|
let jwt = encode(jwtHeader, customJwtBody, 'sharedkey')
|
||||||
|
assert.throws(
|
||||||
|
function() {
|
||||||
|
decode(jwt, pubKeys, ['https://host/oauth/token'])
|
||||||
|
},
|
||||||
|
/No expires set on token/
|
||||||
|
)
|
||||||
|
})
|
||||||
|
t.test('missing iss', function() {
|
||||||
|
let customJwtBody = defaults(jwtBody)
|
||||||
|
delete customJwtBody.iss
|
||||||
|
let jwt = encode(jwtHeader, customJwtBody, 'sharedkey')
|
||||||
|
assert.throws(
|
||||||
|
function() {
|
||||||
|
decode(jwt, pubKeys, ['https://host/oauth/token'])
|
||||||
|
},
|
||||||
|
/No issuer set/
|
||||||
|
)
|
||||||
|
})
|
||||||
|
t.test('iat invalid', function() {
|
||||||
|
let customJwtBody = defaults(jwtBody)
|
||||||
|
customJwtBody.iat += 1200
|
||||||
|
let jwt = encode(jwtHeader, customJwtBody, 'sharedkey')
|
||||||
|
assert.throws(
|
||||||
|
function() {
|
||||||
|
decode(jwt, pubKeys, ['https://host/oauth/token'])
|
||||||
|
},
|
||||||
|
/Issued at in the future by more than 300 seconds/
|
||||||
|
)
|
||||||
|
})
|
||||||
|
t.test('nbf invalid', function() {
|
||||||
|
let customJwtBody = defaults(jwtBody)
|
||||||
|
customJwtBody.nbf = customJwtBody.iat + 1200
|
||||||
|
let jwt = encode(jwtHeader, customJwtBody, 'sharedkey')
|
||||||
|
assert.throws(
|
||||||
|
function() {
|
||||||
|
decode(jwt, pubKeys, ['https://host/oauth/token'])
|
||||||
|
},
|
||||||
|
/Not before in the future by more than 300 seconds/
|
||||||
|
)
|
||||||
|
})
|
||||||
|
t.test('unknown issuer', function() {
|
||||||
|
let customJwtBody = defaults(jwtBody)
|
||||||
|
customJwtBody.iss = 'unknown@test.com'
|
||||||
|
let jwt = encode(jwtHeader, customJwtBody, 'sharedkey')
|
||||||
|
assert.throws(
|
||||||
|
function() {
|
||||||
|
decode(jwt, pubKeys, ['https://host/oauth/token'])
|
||||||
|
},
|
||||||
|
/Unknown issuer 'unknown@test.com'/
|
||||||
|
)
|
||||||
|
})
|
||||||
|
t.test('wrong alg', function() {
|
||||||
|
let customJwtHeader = defaults(jwtHeader)
|
||||||
|
customJwtHeader.alg = 'HS128'
|
||||||
|
assert.throws(
|
||||||
|
function() {
|
||||||
|
encode(customJwtHeader, jwtBody, 'sharedkey')
|
||||||
|
},
|
||||||
|
/Only alg HS256, HS384 and HS512 are supported/
|
||||||
|
)
|
||||||
|
})
|
||||||
|
t.test('unknown kid', function() {
|
||||||
|
let customJwtHeader = defaults(jwtHeader)
|
||||||
|
customJwtHeader.kid = '3'
|
||||||
|
let jwt = encode(customJwtHeader, jwtBody, 'sharedkey')
|
||||||
|
assert.throws(
|
||||||
|
function() {
|
||||||
|
decode(jwt, pubKeys, ['https://host/oauth/token'])
|
||||||
|
},
|
||||||
|
/Unknown pubkey id '3' for this issuer/
|
||||||
|
)
|
||||||
|
})
|
||||||
|
t.test('invalid signature', function() {
|
||||||
|
let customJwtHeader = defaults(jwtHeader)
|
||||||
|
customJwtHeader.kid = '2'
|
||||||
|
let jwt = encode(customJwtHeader, jwtBody, 'sharedkey')
|
||||||
|
let backup = jwt
|
||||||
|
if (jwt[jwt.length - 2] === 'A') {
|
||||||
|
jwt = jwt.slice(0, -2) + 'BB'
|
||||||
|
} else {
|
||||||
|
jwt = jwt.slice(0, -2) + 'AA'
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
assert.throws(
|
||||||
|
function() {
|
||||||
|
decode(jwt, pubKeys, ['https://host/oauth/token'])
|
||||||
|
},
|
||||||
|
/Verification failed with alg 'HS256'/
|
||||||
|
)
|
||||||
|
} catch (err) {
|
||||||
|
console.log('-----')
|
||||||
|
console.log(backup)
|
||||||
|
console.log(jwt)
|
||||||
|
throw err
|
||||||
|
}
|
||||||
|
jwt = encode(customJwtHeader, jwtBody, 'sharedkey')
|
||||||
|
jwt += 'a'
|
||||||
|
assert.throws(
|
||||||
|
function() {
|
||||||
|
decode(jwt, pubKeys, ['https://host/oauth/token'])
|
||||||
|
},
|
||||||
|
/Verification failed with alg 'HS256'/
|
||||||
|
)
|
||||||
|
})
|
||||||
|
t.test('invalid shared key', function() {
|
||||||
|
let customJwtHeader = defaults(jwtHeader)
|
||||||
|
customJwtHeader.kid = '5'
|
||||||
|
customJwtHeader.alg = 'HS256'
|
||||||
|
let jwt = encode(customJwtHeader, jwtBody, 'sharedkey')
|
||||||
|
assert.throws(
|
||||||
|
function() {
|
||||||
|
decode(jwt, pubKeys, ['https://host/oauth/token'])
|
||||||
|
},
|
||||||
|
/Verification failed with alg 'HS256'/
|
||||||
|
)
|
||||||
|
})
|
||||||
|
t.test('invalid pubkey', function() {
|
||||||
|
let customJwtHeader = defaults(jwtHeader)
|
||||||
|
customJwtHeader.kid = '4'
|
||||||
|
let jwt = encode(customJwtHeader, jwtBody, 'sharedkey')
|
||||||
|
assert.throws(
|
||||||
|
function() {
|
||||||
|
decode(jwt, pubKeys, ['https://host/oauth/token'])
|
||||||
|
}
|
||||||
|
)
|
||||||
|
let customJwtBody = defaults(jwtBody)
|
||||||
|
customJwtBody.iss = 'test@custom.com'
|
||||||
|
customJwtHeader.kid = '3'
|
||||||
|
jwt = encode(customJwtHeader, customJwtBody, 'sharedkey')
|
||||||
|
assert.throws(
|
||||||
|
function() {
|
||||||
|
decode(jwt, pubKeys, ['https://host/oauth/token'])
|
||||||
|
},
|
||||||
|
/'3'.+misconfigured/
|
||||||
|
)
|
||||||
|
customJwtHeader.kid = '4'
|
||||||
|
jwt = encode(customJwtHeader, customJwtBody, 'sharedkey')
|
||||||
|
assert.throws(
|
||||||
|
function() {
|
||||||
|
decode(jwt, pubKeys, ['https://host/oauth/token'])
|
||||||
|
},
|
||||||
|
/'4'.+misconfigured/
|
||||||
|
)
|
||||||
|
})
|
||||||
|
t.test('success with broken token', function() {
|
||||||
|
let expectedJwtBody = {
|
||||||
|
id: 1,
|
||||||
|
exp: 1519802991,
|
||||||
|
iat: 1519802691,
|
||||||
|
iss: 'test@test.com',
|
||||||
|
aud: 'https://host/oauth/token'
|
||||||
|
}
|
||||||
|
let decodedJwtBody = decode(
|
||||||
|
'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6MSwiaWF0IjoxNTE5ODAyNjkxfQ.p6t378Ri2JpOCm9WtC36ttyH8ILzG9-OWT_kgMrrRfo',
|
||||||
|
pubKeys,
|
||||||
|
['https://host/oauth/token'],
|
||||||
|
{
|
||||||
|
fixup: (header, body) => {
|
||||||
|
header.kid = '2'
|
||||||
|
body.iss = 'test@test.com'
|
||||||
|
body.aud = 'https://host/oauth/token'
|
||||||
|
body.exp = body.iat + 300
|
||||||
|
},
|
||||||
|
expiresSkew: 307584000
|
||||||
|
}
|
||||||
|
)
|
||||||
|
assert.deepStrictEqual(decodedJwtBody, expectedJwtBody)
|
||||||
|
})
|
||||||
|
})
|
|
@ -1,22 +0,0 @@
|
||||||
import assert from 'assert-extended'
|
|
||||||
|
|
||||||
import * as server from './helper.server'
|
|
||||||
|
|
||||||
describe('Server', () => {
|
|
||||||
let client
|
|
||||||
|
|
||||||
beforeEach(() => {
|
|
||||||
client = server.createClient()
|
|
||||||
})
|
|
||||||
|
|
||||||
it('should run', () =>
|
|
||||||
assert.isFulfilled(
|
|
||||||
client.getAsync('/')
|
|
||||||
)
|
|
||||||
.then(data => {
|
|
||||||
assert.ok(data)
|
|
||||||
assert.ok(data.name)
|
|
||||||
assert.ok(data.version)
|
|
||||||
})
|
|
||||||
)
|
|
||||||
})
|
|
28
test/server.test.mjs
Normal file
28
test/server.test.mjs
Normal file
|
@ -0,0 +1,28 @@
|
||||||
|
import { Eltro as t, assert} from 'eltro'
|
||||||
|
|
||||||
|
import * as server from './helper.server.mjs'
|
||||||
|
|
||||||
|
t.describe('Server', function() {
|
||||||
|
let client
|
||||||
|
|
||||||
|
t.before(function() {
|
||||||
|
client = server.createClient()
|
||||||
|
})
|
||||||
|
|
||||||
|
t.test('should run', async function() {
|
||||||
|
let data = await client.get('/')
|
||||||
|
|
||||||
|
assert.ok(data)
|
||||||
|
assert.ok(data.name)
|
||||||
|
assert.ok(data.version)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.test('should handle errors fine', async function() {
|
||||||
|
let data = await assert.isRejected(client.get('/error'))
|
||||||
|
|
||||||
|
assert.ok(data)
|
||||||
|
assert.ok(data.body)
|
||||||
|
assert.strictEqual(data.body.status, 422)
|
||||||
|
assert.match(data.body.message, /test/)
|
||||||
|
})
|
||||||
|
})
|
Loading…
Reference in a new issue