• 设为首页
  • 点击收藏
  • 手机版
    手机扫一扫访问
    迪恩网络手机版
  • 关注官方公众号
    微信扫一扫关注
    迪恩网络公众号

danneu/koa-bouncer: ⛔ an http param validator for koa apps

原作者: [db:作者] 来自: 网络 收藏 邀请

开源软件名称:

danneu/koa-bouncer

开源软件地址:

https://github.com/danneu/koa-bouncer

开源编程语言:

JavaScript 100.0%

开源软件介绍:

koa-bouncer

Build Status NPM version Dependency Status

NPM

An http parameter validation library for Koa web apps.

npm install --save koa-bouncer

Inspired by RocksonZeta.

Works best with koa-router for routing.

If you'd like to see how koa-bouncer looks in a real (demo) Koa application, check out my koa-skeleton repository.

Example

Using koa-router for routing

const Koa = require('koa');
const Router = require('koa-router')
const bouncer = require('koa-bouncer');
const app = new Koa();
const router = new Router();

// extends the Koa context with some methods
app.use(bouncer.middleware());

// POST /users - create user endpoint
router.post('/users', async (ctx) => {

  // validate input

  ctx.validateBody('uname')
    .required('Username required')
    .isString()
    .trim()

  ctx.validateBody('email')
    .optional()
    .isString()
    .trim()
    .isEmail('Invalid email format')

  ctx.validateBody('password1')
    .required('Password required')
    .isString()
    .isLength(6, 100, 'Password must be 6-100 chars')

  ctx.validateBody('password2')
    .required('Password confirmation required')
    .isString()
    .eq(ctx.vals.password1, 'Passwords must match')

  // running database query last to give the other validations a chance to fail
  ctx.validateBody('uname')
    .check(await db.findUserByUname(ctx.vals.uname), 'Username taken')

  // if we get this far, then validation succeeded.

  // the validation populates a `ctx.vals` object with validated values
  //=> { uname: 'foo', password1: 'secret', password2: 'secret' }
  console.log(ctx.vals)

  const user = await db.insertUser({
    uname: ctx.vals.uname,
    email: ctx.vals.email,
    password: ctx.vals.password1
  })

  ctx.redirect(`/users/${user.id}`)
})

app
  .use(router.routes())
  .use(router.allowedMethods());
  

The general idea

The idea is that koa-bouncer exposes methods that transform and assert against user-input (form submissions, request bodies, query strings) within your routes.

If an assertion fails, then koa-bouncer throws a bouncer.ValidationError that you can catch upstream. For example, maybe you want to redirect back to the form, show a tip, and repopulate the form with the user's progress.

If validation succeeds, then you can access the validated/transformed parameters in a ctx.vals map that gets populated during validation.

Usage

First, you need to inject bouncer's middleware:

bouncer.middleware(opts)

This extends the Koa context with these methods for you to use in routes, the bulk of the koa-bouncer abstraction:

  • ctx.validateParam(key) => Validator
  • ctx.validateQuery(key) => Validator
  • ctx.validateBody(key) => Validator
  • ctx.check(value, [tip]) => throws ValidationError if falsey
  • ctx.checkNot(value, [tip]) => throws ValidationError if truthy

The first three methods return a validator that targets the value in the url param, query param, or body param that you specified with 'key'.

When you spawn a validator, it immediately populates ctx.vals[key] with the initial value of the parameter. You can then chain methods like .toString().trim().isEmail() to transform the value in ctx.vals and make assertions against it.

Just by calling these methods, they will begin populating ctx.vals:

router.get('/search', async (ctx) => {
  ctx.validateQuery('keyword')
  ctx.validateQuery('sort')
  ctx.body = JSON.stringify(ctx.vals)
})
curl http://localhost:3000/search
=> {}

curl http://localhost:3000/search?sort=age
=> { "sort": "age" }

We can use .required() to throw a ValidationError when the parameter is undefined. For example, we can decide that you must always supply a ?keyword= to our search endpoint.

And we can use .optional() to only run the chained validations/assertions if the parameter is undefined (not given by user) or if it is an empty string.

router.get('/search', async (ctx) => {
  ctx.validateQuery('keyword').required().isString().trim()
  ctx.validateQuery('sort').toArray()
  ctx.body = JSON.stringify(ctx.vals)
})
curl http://localhost:3000/search
=> Uncaught ValidationError

curl http://localhost:3000/search?keyword=hello
=> { "keyword": "hello", "sort": [] }

curl http://localhost:3000/search?keyword=hello&sort=age
=> { "keyword": "hello", "sort": ["age"] }

curl http://localhost:3000/search?keyword=hello&sort=age&sort=height
=> { "keyword": "hello", "sort": ["age", "height"] }

If a validation fails, then the validator throws a bouncer.ValidationError that we can catch with upstream middleware.

For example, we can decide that upon validation error, we redirect the user back to whatever the previous page was and populate a temporary flash object with the error and their parameters so that we can repopulate the form.

app.use(async (ctx, next) => {
  try {
    await next();
  } catch(err) {
    if (err instanceof bouncer.ValidationError) {
      ctx.flash = {
        message: ['danger', err.message],
        params: ctx.request.body
      };
      return ctx.redirect('back');
    }
    throw err;
  }
});

router.post('/users', async (ctx) => {
  ctx.validateBody('username')
    .required('Username is required')
    .isString()
    .trim()
    .isLength(3, 15, 'Username must be 3-15 chars');

  const user = await database.insertUser(ctx.vals.username);
  ctx.body = 'You successfully registered';
});
http --form POST localhost:3000/users
=> 302 Redirect to GET /users, message='Username is required'

http --form POST localhost:3000/users username=bo
=> 302 Redirect to GET /users, message='Username must be 3-15 chars'

http --form POST localhost:3000/users username=freeman
=> 200 OK, You successfully registered

You can pass options into the bouncer.middleware() function.

Here are the default ones:

app.use(bouncer.middleware({
  getParams({params}) { return params; },
  getQuery({query}) { return query; },
  getBody({request}) { return request.body; }
}));

You can override these if the validators need to look in a different place to fetch the respective keys when calling the validateParam, validateQuery, and validateBody methods.

You can always define custom validators via Validator.addMethod:

const Validator = require('koa-bouncer').Validator;

Validator.addMethod('isValidBitcoinAddress', function(tip = 'Invalid Bitcoin address') {
  // Will thread the tip through the nested assertions
  this
    .isString(tip)
    .trim()
    // Must be alphanumeric from start to finish
    .match(/^[a-z0-9]+$/i, tip)
    // But must not contain any of these chars
    .notMatch(/[0O1l]/, tip);

  return this;
});

Maybe put that in a custom_validations.js file and remember to load it.

Now you can use the custom validator method in a route or middleware:

ctx.validateBody('address')
  .required()
  .isValidBitcoinAddress();

These chains always return the underlying validator instance. You can access its value at any instant with .val().

const validator = ctx.validateBody('address')
  .required()
  .isValidBitcoinAddress();

console.log("current value of ctx.vals['address'] is", validator.val());

Here's how you'd write a validator method that transforms the underlying value:

Validator.addMethod('add10', function() {
  this.tap(val => val + 10);
  return this;
});

In other words, just use this.set(newVal) to update the object of validated params. And remember to return this so that you can continue chaining things on to the validator.

Validator methods

.val()

Returns the current value currently inside the validator.

router.get('/search', async (ctx) => {
  const validator1 = ctx.validateQuery('q').required();
  const validator2 = ctx.validateQuery('sort').optional();

  ctx.body = JSON.stringify([validator1.val(), validator2.val()]);
});
curl http://localhost:3000/search?q=hello&sort=created_at
// 200 OK ["hello", "created_at"]

I rarely use this method inside a route and prefer to access values from the ctx.vals object. So far I only use it internally when implementing validator functions.

.required([tip])

Only fails if val is undefined. Required the user to at least provie

ctx.validateBody('username')
  .required('Must provide username')

.optional()

If val is undefined or if it an empty string (after being trimmed) at this point, then skip over the rest of the methods.

This is so that you can validate a val only if user provided one.

ctx.validateBody('email')
  .optional()
  .isEmail('Invalid email format') // Only called if ctx.request.body is `undefined`
ctx.validateBody('email')
  .tap(x => '[email protected]')
  .optional()
  .isEmail()  // Always called since we are ensuring that val is always defined

Mutating ctx.vals to define a val inside an optional validator will turn off the validator's validator.isOptional() flag.

ctx.validateBody('email').optional();
ctx.vals.email = '[email protected]';
ctx.validateBody('email').isEmail();  // This will run

You can see the optional state of a validator with its .isOptional() method:

const validator = ctx.validateBody('email').optional();
console.log(validator.isOptional());  //=> true
ctx.vals.email = '[email protected]';
console.log(validator.isOptional());  //=> false
validator.isEmail();  // This will run

The reason koa-bouncer considers empty strings to be unset (instead of just undefined) is because the browser sends empty strings for text inputs. This is usually the behavior you want.

Also, note that .required() only fails if the value is undefined. It succeeds on empty string. This is also usually the behavior you want.

.isIn(array, [tip])

Ensure val is included in given array (=== comparison).

ctx.validateBody('role')
  .required('Must provide a role')
  .isIn(['banned', 'member', 'mod', 'admin'], 'Invalid role')

.isNotIn(array, [tip])

Ensure val is not included in given array (=== comparison).

ctx.validateBody('favorite-fruit')
  .isNotIn(['apple', 'pomegranate'], 'You cannot choose forbidden fruit')

.defaultTo(defaultVal)

If val is undefined, set it to defaultVal.

ctx.validateBody('multiplier')
  .defaultTo(1.0)
  .toFiniteFloat('multiplier must be a valid number')

.isString([tip])

Ensure val is a string.

Note: Also works with strings created via new String() where typeof new String() === 'object'.

ctx.validateBody('username')
  .isString()

It's a good practice to always call one of the .is* methods since they add explicit clarity to the validation step.

.isArray([tip])

Ensure val is an Array.

ctx.validateQuery('recipients')
  .isArray('recipients must be an array')
curl http://localhost:3000/?recipients=joey
=> ValidationError

curl http://localhost:3000/?recipients=joey&recipients=kate&recipients=max
=> 200 OK, ctx.vals => ['joey', 'kate', 'max']

Note: The previous example can be improved with .toArray.

ctx.validateQuery('recipients')
  .toArray()
  .isArray('recipients must be an array')
curl http://localhost:3000/?recipients=joey
=> 200 OK, ctx.vals.recipients => ['joey']

curl http://localhost:3000/?recipients=joey&recipients=kate&recipients=max
=> 200 OK, ctx.vals.recipients => ['joey', 'kate', 'max']

.eq(otherVal::Number, [tip])

Ensures val === otherVal.

ctx.validateBody('house-edge')
  .eq(0.01, 'House edge must be 1%')

.gt(otherVal::Number, [tip])

Ensures val > otherVal.


鲜花

握手

雷人

路过

鸡蛋
该文章已有0人参与评论

请发表评论

全部评论

专题导读
热门推荐
阅读排行榜

扫描微信二维码

查看手机版网站

随时了解更新最新资讯

139-2527-9053

在线客服(服务时间 9:00~18:00)

在线QQ客服
地址:深圳市南山区西丽大学城创智工业园
电邮:jeky_zhao#qq.com
移动电话:139-2527-9053

Powered by 互联科技 X3.4© 2001-2213 极客世界.|Sitemap