├── .gitignore ├── .travis.yml ├── history.md ├── index.js ├── package.json ├── readme.md └── test └── index.js /.gitignore: -------------------------------------------------------------------------------- 1 | lib-cov 2 | *.seed 3 | *.log 4 | *.csv 5 | *.dat 6 | *.out 7 | *.pid 8 | *.gz 9 | 10 | pids 11 | logs 12 | results 13 | 14 | node_modules 15 | npm-debug.log -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - "6" 4 | - "8" 5 | - "10" 6 | -------------------------------------------------------------------------------- /history.md: -------------------------------------------------------------------------------- 1 | 3.1.1 / 2015-02-04 2 | ================== 3 | 4 | Fix use of deprecated `Promise.from` method (only impacts use of the async api) 5 | 6 | 3.1.0 / 2014-11-03 7 | ================== 8 | 9 | * Update dependencies 10 | * Add `matchRelativePaths` option 11 | 12 | 3.0.3 / 2014-04-22 13 | ================== 14 | 15 | * Update path-to-regexp to 0.1.2 16 | 17 | 3.0.2 / 2014-02-19 18 | ================== 19 | 20 | * Fix another bug in use3 21 | 22 | 3.0.1 / 2014-02-19 23 | ================== 24 | 25 | * Fix bug in use3 26 | 27 | 3.0.0 / 2014-02-19 28 | ================== 29 | 30 | * Complete redesign so it's not a singleton anymore 31 | * Don't set the user when anonymous (this confused way too many people) 32 | 33 | 2.1.0 / 2013-06-13 34 | ================== 35 | 36 | * fix: route handlers would continue falling through even when `true` was returned (thanks to [@doughsay](https://github.com/doughsay) for reporting and helping with the fix) 37 | * Use npm versions of dependencies, rather than my own GitHub forks 38 | 39 | 2.0.3 / 2013-02-10 40 | ================== 41 | 42 | * fix: extra `/` sometimes appeared in paths 43 | 44 | 2.0.2 / 2013-02-08 45 | ================== 46 | 47 | * fix: pass the keys array to pathToRegexp 48 | 49 | 2.0.1 / 2013-02-08 50 | ================== 51 | 52 | * fix: prepend app.path for mounted apps so handlers always use the full path. 53 | 54 | 2.0.0 / 2013-02-08 55 | ================== 56 | 57 | * redesign API 58 | 59 | 1.0.1 / 2012-12-27 60 | ================== 61 | 62 | * fix: throw real errors not strings 63 | 64 | 1.0.0 / 2012-08-24 65 | ================== 66 | 67 | * First Stable Release 68 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | var Promise = require('promise'); 4 | var ert = require('ert'); 5 | var pathToRegexp = require('path-to-regexp'); 6 | 7 | module.exports = ConnectRoles; 8 | function ConnectRoles(options) { 9 | options = options || {}; 10 | this.functionList = []; 11 | this.failureHandler = options.failureHandler || defaultFailureHandler; 12 | this.async = options.async || false; 13 | this.userProperty = options.userProperty || 'user'; 14 | this.matchRelativePaths = options.matchRelativePaths || false; 15 | } 16 | 17 | ConnectRoles.prototype.use = function () { 18 | if (arguments.length === 1) { 19 | this.use1(arguments[0]); 20 | } else if (arguments.length === 2) { 21 | this.use2(arguments[0], arguments[1]); 22 | } else if (arguments.length === 3) { 23 | this.use3(arguments[0], arguments[1], arguments[2]); 24 | } else { 25 | throw new Error('use can have 1, 2 or 3 arguments, not ' + arguments.length); 26 | } 27 | }; 28 | 29 | ConnectRoles.prototype.use1 = function (fn) { 30 | if (typeof fn !== 'function') throw new TypeError('Expected fn to be of type function'); 31 | this.functionList.push(fn); 32 | }; 33 | 34 | ConnectRoles.prototype.use2 = function (action, fn) { 35 | if (typeof action !== 'string') throw new TypeError('Expected action to be of type string'); 36 | if (action[0] === '/') throw new TypeError('action can\'t start with `/`'); 37 | this.use1(function (req, act) { 38 | if (act === action) { 39 | return fn(req); 40 | } 41 | }); 42 | }; 43 | ConnectRoles.prototype.use3 = function (action, path, fn) { 44 | var self = this; 45 | if (typeof path !== 'string') throw new Error('Expected path to be of type string'); 46 | var keys = []; 47 | var exp = pathToRegexp(path, keys); 48 | this.use2(action, function (req) { 49 | var pathToMatch = null; 50 | if(self.matchRelativePaths === true) { 51 | pathToMatch = req.url; 52 | } else { 53 | pathToMatch = req.app.path().replace(/\/$/, '') + req.path; 54 | } 55 | 56 | var match; 57 | if (match = exp.exec(pathToMatch)) { 58 | req = Object.create(req); 59 | req.params = Object.create(req.params || {}); 60 | keys.forEach(function (key, i) { 61 | req.params[key.name] = match[i + 1]; 62 | }); 63 | return fn(req); 64 | } 65 | }); 66 | } 67 | 68 | ConnectRoles.prototype.can = routeTester('can'); 69 | ConnectRoles.prototype.is = routeTester('is'); 70 | ConnectRoles.prototype.isAuthenticated = function () { 71 | var msg = 'Expected req.isAuthenticated to be a function. ' 72 | + 'If you are using passport, make sure the passport ' 73 | + 'middleware comes first'; 74 | var res = function (req, res, next) { 75 | if (typeof req.isAuthenticated !== 'function') { 76 | throw new Error(msg); 77 | } 78 | if (req.isAuthenticated()) return next(); 79 | else return this.failureHandler(req, res, "isAuthenticated"); 80 | }.bind(this); 81 | res.here = function (req, res, next) { 82 | if (typeof req.isAuthenticated !== 'function') { 83 | throw new Error(msg); 84 | } 85 | if (req.isAuthenticated()) return next(); 86 | else return next('route'); 87 | }.bind(this); 88 | return res; 89 | }; 90 | ConnectRoles.prototype.test = function (req, action) { 91 | if (this.async) { 92 | return this.functionList.reduce(function (accumulator, fn) { 93 | return accumulator.then(function (result) { 94 | if (typeof result === 'boolean') return result; 95 | else return fn(req, action); 96 | }); 97 | }, Promise.resolve(null)).then(function (result) { 98 | if (typeof result == 'boolean') return result; 99 | else return false; 100 | }); 101 | } else { 102 | for (var i = 0; i < this.functionList.length; i++){ 103 | var fn = this.functionList[i]; 104 | var vote = fn(req, action); 105 | if (typeof vote === 'boolean') { 106 | return vote; 107 | } 108 | } 109 | return false; 110 | } 111 | }; 112 | ConnectRoles.prototype.middleware = function (options) { 113 | options = options || {}; 114 | var userProperty = options.userProperty || this.userProperty; 115 | return function (req, res, next) { 116 | if (req[userProperty] && res.locals && !res.locals[userProperty]) 117 | res.locals[userProperty] = req[userProperty]; 118 | if (req[userProperty]) { 119 | req[userProperty].is = tester(this, req,'is'); 120 | req[userProperty].can = tester(this, req,'can'); 121 | } 122 | req.userIs = tester(this, req, 'is'); 123 | req.userCan = tester(this, req, 'can'); 124 | if (res.locals) { 125 | res.locals.userIs = tester(this, req, 'is'); 126 | res.locals.userCan = tester(this, req, 'can'); 127 | if (typeof req.isAuthenticated === 'function') 128 | res.locals.isAuthenticated = req.isAuthenticated.bind(req); 129 | } 130 | next(); 131 | }.bind(this); 132 | }; 133 | function tester(roles, req, verb) { 134 | return function (action) { 135 | var act = ert(req, action); 136 | return roles.test(req, act) 137 | } 138 | } 139 | 140 | function routeTester(verb) { 141 | return function (action){ 142 | function handle(onFail) { 143 | return function (req, res, next) { 144 | var act = ert(req, action); 145 | if (this.async) { 146 | this.test(req, act).done(function (result) { 147 | if (result) { 148 | next(); 149 | } else { 150 | //Failed authentication. 151 | onFail(req, res, next, act); 152 | } 153 | }.bind(this), next); 154 | } else { 155 | if(this.test(req, act)){ 156 | next(); 157 | }else{ 158 | //Failed authentication. 159 | onFail(req, res, next, act); 160 | } 161 | } 162 | }.bind(this); 163 | } 164 | var failureHandler = this.failureHandler; 165 | var result = handle.call(this, function (req, res, next, act) { 166 | failureHandler(req, res, act); 167 | }); 168 | result.here = handle.call(this, function (req, res, next) { 169 | next('route'); 170 | }); 171 | return result; 172 | }; 173 | } 174 | 175 | function defaultFailureHandler(req, res, action) { 176 | (res.sendStatus || res.send).bind(res)(403); 177 | } 178 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "connect-roles", 3 | "description": "Provides dynamic roles based authorization for node.js connect and express servers.", 4 | "version": "3.1.2", 5 | "homepage": "http://documentup.com/ForbesLindesay/connect-roles", 6 | "repository": { 7 | "type": "git", 8 | "url": "git://github.com/ForbesLindesay/connect-roles.git" 9 | }, 10 | "main": "./index", 11 | "devDependencies": { 12 | "mocha": "*" 13 | }, 14 | "scripts": { 15 | "test": "mocha -R spec" 16 | }, 17 | "author": "Forbes Lindesay", 18 | "license": "BSD", 19 | "keywords": [ 20 | "roles", 21 | "authorization", 22 | "authentication", 23 | "security", 24 | "connect", 25 | "express", 26 | "passport", 27 | "everyauth" 28 | ], 29 | "dependencies": { 30 | "path-to-regexp": "^3.0.0", 31 | "ert": "^1.0.1", 32 | "promise": "^8.0.1" 33 | } 34 | } -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | # Connect Roles 2 | 3 | Sponsor 4 | 5 | 6 | 7 | Connect roles is designed to work with connect or express. It is an authorisation provider, not an authentication provider. It is designed to support context sensitive roles/abilities, through the use of middleware style authorisation strategies. 8 | 9 | If you're looking for an authentication system I suggest you check out [passport.js](https://github.com/jaredhanson/passport), which works perfectly with this module. 10 | 11 | [![Build Status](https://secure.travis-ci.org/ForbesLindesay/connect-roles.png?branch=master)](http://travis-ci.org/ForbesLindesay/connect-roles) 12 | [![Dependency Status](https://img.shields.io/david/ForbesLindesay/connect-roles.svg)](https://david-dm.org/ForbesLindesay/connect-roles) 13 | [![NPM version](https://img.shields.io/npm/v/connect-roles.svg)](https://www.npmjs.com/package/connect-roles) 14 | 15 | ## Installation 16 | 17 | $ npm install connect-roles 18 | 19 | ## Usage 20 | 21 | ```javascript 22 | var authentication = require('your-authentication-module-here'); 23 | var ConnectRoles = require('connect-roles'); 24 | var express = require('express'); 25 | var app = express(); 26 | 27 | var user = new ConnectRoles({ 28 | failureHandler: function (req, res, action) { 29 | // optional function to customise code that runs when 30 | // user fails authorisation 31 | var accept = req.headers.accept || ''; 32 | res.status(403); 33 | if (~accept.indexOf('html')) { 34 | res.render('access-denied', {action: action}); 35 | } else { 36 | res.send('Access Denied - You don\'t have permission to: ' + action); 37 | } 38 | } 39 | }); 40 | 41 | app.use(authentication) 42 | app.use(user.middleware()); 43 | 44 | //anonymous users can only access the home page 45 | //returning false stops any more rules from being 46 | //considered 47 | user.use(function (req, action) { 48 | if (!req.isAuthenticated()) return action === 'access home page'; 49 | }) 50 | 51 | //moderator users can access private page, but 52 | //they might not be the only ones so we don't return 53 | //false if the user isn't a moderator 54 | user.use('access private page', function (req) { 55 | if (req.user.role === 'moderator') { 56 | return true; 57 | } 58 | }) 59 | 60 | //admin users can access all pages 61 | user.use(function (req) { 62 | if (req.user.role === 'admin') { 63 | return true; 64 | } 65 | }); 66 | 67 | 68 | app.get('/', user.can('access home page'), function (req, res) { 69 | res.render('private'); 70 | }); 71 | app.get('/private', user.can('access private page'), function (req, res) { 72 | res.render('private'); 73 | }); 74 | app.get('/admin', user.can('access admin page'), function (req, res) { 75 | res.render('admin'); 76 | }); 77 | 78 | app.listen(3000); 79 | ``` 80 | 81 | ## API 82 | 83 | To access all methods, you must construct an instance via: 84 | 85 | ```js 86 | var ConnectRoles = require('connect-roles'); 87 | var roles = new ConnectRoles(options); 88 | ``` 89 | 90 | options: 91 | 92 | - failureHandler {Function} - a function that takes (req, res) when the user has failed authorisation 93 | - async {Boolean} - experimental support for async rules 94 | - userProperty {String} - the property name for the user object on req. Defaults to "user" 95 | - matchRelativePaths {Boolean} - by default, rules use absolute paths from the root of the application. 96 | 97 | ### roles.use(fn(req, action)) 98 | 99 | Define an authorisation strategy which takes the current request and the action being performed. fn may return `true`, `false` or `undefined`/`null` 100 | 101 | If `true` is returned then no further strategies are considered, and the user is **granted** access. 102 | 103 | If `false` is returned, no further strategies are considered, and the user is **denied** access. 104 | 105 | If `null`/`undefined` is returned, the next strategy is considerd. If it is the last strategy then access is **denied**. 106 | 107 | ### roles.use(action, fn(req)) 108 | 109 | The strategy `fn` is only used when the action is equal to `action`. It has the same behaviour with regards to return values as `roles.use(fn(req, action))` (see above). 110 | 111 | It is equivallent to calling: 112 | 113 | ```javascript 114 | roles.use(function (req, act) { 115 | if (act === action) { 116 | return fn(req); 117 | } 118 | }); 119 | ``` 120 | 121 | **N.B.** The action must not start with a `/` character 122 | 123 | ### roles.use(action, path, fn(req)) 124 | 125 | Path must be an express style route. It will then attach any parameters to `req.params`. 126 | 127 | e.g. 128 | 129 | ```javascript 130 | roles.use('edit user', '/user/:userID', function (req) { 131 | if (req.params.userID === req.user.id) return true; 132 | }); 133 | ``` 134 | 135 | Note that this authorisation strategy will only be used on routes that match `path`. 136 | 137 | It is equivallent to calling: 138 | 139 | ```javascript 140 | var keys = []; 141 | var exp = pathToRegexp(path, key); 142 | roles.use(function (req, act) { 143 | var match; 144 | if (act === action && match = exp.exec(req.path)) { 145 | req = Object.create(req); 146 | req.params = Object.create(req.params || {}); 147 | keys.forEach(function (key, i) { 148 | req.params[key.name] = match[i + 1]; 149 | }); 150 | return fn(req); 151 | } 152 | }); 153 | ``` 154 | 155 | ### roles.can(action) and roles.is(action) 156 | 157 | `can` and `is` are synonyms everywhere they appear. 158 | 159 | You can use these as express route middleware: 160 | 161 | ```javascript 162 | var user = roles; 163 | 164 | app.get('/profile/:id', user.can('edit profile'), function (req, res) { 165 | req.render('profile-edit', { id: req.params.id }); 166 | }) 167 | app.get('/admin', user.is('admin'), function (req, res) { 168 | res.render('admin'); 169 | } 170 | ``` 171 | 172 | If you want to skip only the current routes, you can also use `.here` 173 | 174 | ```js 175 | app.get('/', user.can('see admin page').here, function (req, res, next) { 176 | res.render('admin-home-page'); 177 | }); 178 | app.get('/', function (req, res, next) { 179 | res.render('default-home-page'); 180 | }); 181 | ``` 182 | 183 | ### req.userCan(action) and req.userIs(action) 184 | 185 | `can` and `is` are synonyms everywhere they appear. 186 | 187 | These functions return `true` or `false` depending on whether the user has access. 188 | 189 | e.g. 190 | 191 | ```javascript 192 | app.get('/', function (req, res) { 193 | if (req.userIs('admin')) { 194 | res.render('home/admin'); 195 | } else if (req.userCan('login')) { 196 | res.render('home/login'); 197 | } else { 198 | res.render('home'); 199 | } 200 | }) 201 | ``` 202 | 203 | ### user.can(action) and user.is(action) 204 | 205 | Inside the views of an express application you may use `userCan` and `userIs` which are equivallent to `req.userCan` and `req.userIs` 206 | 207 | e.g. 208 | 209 | ```html 210 | <% if (userCan('impersonate')) { %> 211 | 212 | <% } %> 213 | ``` 214 | 215 | or in jade: 216 | 217 | ```jade 218 | if userCan('impersonate') 219 | button#impersonate Impersonate 220 | ``` 221 | 222 | **N.B.** not displaying a button doesn't mean someone can't do the thing that the button would do if clicked. The view is not where your security should go, but it is important for useability that you don't display buttons that will just result in 'access denied'. 223 | 224 | ## License 225 | 226 | MIT 227 | -------------------------------------------------------------------------------- /test/index.js: -------------------------------------------------------------------------------- 1 | var assert = require('assert'); 2 | var Roles = require('../'); 3 | 4 | function constant(val) { 5 | return function () { 6 | return val; 7 | }; 8 | } 9 | function request(user) { 10 | return { 11 | user: user, 12 | isAuthenticated: constant(user ? true : false) 13 | }; 14 | } 15 | function response() { 16 | return { 17 | locals: {} 18 | }; 19 | } 20 | describe('middleware', function () { 21 | describe('when there is a user', function () { 22 | it('adds methods', function (done) { 23 | var roles = new Roles(); 24 | var req = request({ id: 'Forbes' }); 25 | var res = response(); 26 | roles.middleware()(req, res, function (err) { 27 | if (err) return done(err); 28 | assert.strictEqual(typeof req.user.can, 'function'); 29 | assert.strictEqual(typeof req.user.is, 'function'); 30 | assert.strictEqual(req.user.can('foo'), false); 31 | assert.strictEqual(req.user.is('foo'), false); 32 | assert.strictEqual(typeof req.userCan, 'function'); 33 | assert.strictEqual(typeof req.userIs, 'function'); 34 | assert.strictEqual(req.userCan('foo'), false); 35 | assert.strictEqual(req.userIs('foo'), false); 36 | 37 | assert.strictEqual(res.locals.isAuthenticated(), true); 38 | assert.strictEqual(typeof res.locals.user.can, 'function'); 39 | assert.strictEqual(typeof res.locals.user.is, 'function'); 40 | assert.strictEqual(res.locals.user.can('foo'), false); 41 | assert.strictEqual(res.locals.user.is('foo'), false); 42 | assert.strictEqual(typeof res.locals.userCan, 'function'); 43 | assert.strictEqual(typeof res.locals.userIs, 'function'); 44 | assert.strictEqual(res.locals.userCan('foo'), false); 45 | assert.strictEqual(res.locals.userIs('foo'), false); 46 | done(); 47 | }); 48 | }); 49 | }); 50 | describe('when there is no user', function () { 51 | it('adds methods and the anonymous user', function (done) { 52 | var roles = new Roles(); 53 | var req = request(); 54 | var res = response(); 55 | roles.middleware()(req, res, function (err) { 56 | if (err) return done(err); 57 | assert.strictEqual(typeof req.userCan, 'function'); 58 | assert.strictEqual(typeof req.userIs, 'function'); 59 | assert.strictEqual(req.userCan('foo'), false); 60 | assert.strictEqual(req.userIs('foo'), false); 61 | 62 | assert.strictEqual(res.locals.isAuthenticated(), false); 63 | assert.strictEqual(typeof res.locals.userCan, 'function'); 64 | assert.strictEqual(typeof res.locals.userIs, 'function'); 65 | assert.strictEqual(res.locals.userCan('foo'), false); 66 | assert.strictEqual(res.locals.userIs('foo'), false); 67 | done(); 68 | }); 69 | }); 70 | }); 71 | }); 72 | 73 | function notCalled(name) { 74 | return function notCalled() { 75 | throw new Error('The function ' + name + ' should not be called here.'); 76 | } 77 | } 78 | describe('isAuthenticated route middleware', function () { 79 | describe('when there is a user', function () { 80 | it('passes the test', function (done) { 81 | var roles = new Roles({ 82 | failureHandler: notCalled('Failure Handler') 83 | }); 84 | var req = request({id: 'Forbes'}); 85 | var res = {send: notCalled('send')}; 86 | roles.isAuthenticated()(req, res, function (err) { 87 | if (err) return done(err); 88 | done(); 89 | }); 90 | }); 91 | }); 92 | describe('when there is no user.', function () { 93 | it('fails the test', function (done) { 94 | var roles = new Roles(); 95 | function send(code) { 96 | assert.strictEqual(code, 403); 97 | done(); 98 | } 99 | var req = request(); 100 | var res = {send: send}; 101 | roles.isAuthenticated()(req, res, notCalled('next')); 102 | }); 103 | it('calls the failure handler', function (done) { 104 | var roles = new Roles({ 105 | failureHandler: function (request, response, action) { 106 | assert.strictEqual(request, req); 107 | assert.strictEqual(response, res); 108 | assert.strictEqual(action, 'isAuthenticated'); 109 | done(); 110 | } 111 | }); 112 | var req = request(); 113 | var res = {}; 114 | roles.isAuthenticated()(req, res, notCalled('next')); 115 | }); 116 | }); 117 | }); 118 | describe('can middleware', function () { 119 | describe('when the user is authenticated', function () { 120 | it('passes the test', function (done) { 121 | var roles = new Roles(); 122 | var req = request(); 123 | var res = {}; 124 | roles.use(function (req, action) { assert.strictEqual(action, 'any'); return true; }); 125 | roles.can('any')(req, res, function (err) { 126 | if (err) return done(err); 127 | done(); 128 | }); 129 | }); 130 | }); 131 | describe('when the user is not authenticated', function () { 132 | it('fails the test', function (done) { 133 | var roles = new Roles(); 134 | roles.use(function (req, action) { assert.strictEqual(action, 'any'); return false; }); 135 | function send(code) { 136 | assert.strictEqual(code, 403); 137 | done(); 138 | } 139 | var req = request(); 140 | var res = {send: send}; 141 | roles.can('any')(req, res, notCalled('next')); 142 | }); 143 | it('calls the failure handler', function (done) { 144 | var roles = new Roles({ 145 | failureHandler: function (request, response, action) { 146 | assert.strictEqual(request, req); 147 | assert.strictEqual(response, res); 148 | assert.strictEqual(action, 'any'); 149 | done(); 150 | } 151 | }); 152 | roles.use(function (req, action) { assert.strictEqual(action, 'any'); return false; }); 153 | var req = request(); 154 | var res = {}; 155 | roles.can('any')(req, res, notCalled('next')); 156 | }); 157 | }); 158 | }); 159 | 160 | describe('when there are no handlers', function () { 161 | it('requests are rejected by default', function () { 162 | var roles = new Roles(); 163 | assert.strictEqual(roles.test(request(), 'any'), false); 164 | }); 165 | }); 166 | describe('when there are no handlers that return `true` or `false`', function () { 167 | it('requests are rejected by default', function () { 168 | var roles = new Roles(); 169 | var called = false; 170 | roles.use(function (req, action) { assert(action === 'any'); called = true; }); 171 | assert.strictEqual(roles.test(request(), 'any'), false); 172 | assert.strictEqual(called, true); 173 | }); 174 | }); 175 | describe('when the first handler to return a value returns `false`', function () { 176 | it('requests are rejected', function () { 177 | var roles = new Roles(); 178 | roles.use(function (req, action) { assert(action === 'any'); }); 179 | roles.use(function (req, action) { assert(action === 'any'); return false; }); 180 | roles.use(function (req, action) { assert(action === 'any'); return true; }); 181 | assert.strictEqual(roles.test(request(), 'any'), false); 182 | }); 183 | }); 184 | describe('when the first handler to return a value returns `true`', function () { 185 | it('requests are accepted', function () { 186 | var roles = new Roles(); 187 | roles.use(function (req, action) { assert(action === 'any'); }); 188 | roles.use(function (req, action) { assert(action === 'any'); return true; }); 189 | roles.use(function (req, action) { assert(action === 'any'); return false; }); 190 | assert.strictEqual(roles.test(request(), 'any'), true); 191 | }); 192 | }); 193 | --------------------------------------------------------------------------------