├── .eslintignore ├── index.js ├── templates ├── withPartials │ ├── footer.html │ ├── head.html │ └── account.html ├── helpers │ ├── numberformat.js │ └── jsonstring.js ├── account.html ├── license.html └── index.html ├── .gitignore ├── public ├── images │ ├── microsoft.svg │ ├── facebook.svg │ ├── instagram.svg │ ├── twitter.svg │ ├── linkedin.svg │ ├── google.svg │ ├── evernote.svg │ ├── foursquare.svg │ ├── bitbucket.svg │ ├── github.svg │ └── dropbox.svg └── css │ ├── buttons-si-ie8.css │ ├── buttons-si.css │ ├── examples.css │ ├── github.svg │ └── styles.css ├── .eslintrc.json ├── lib ├── utilities.js ├── maths.js ├── store.js ├── handlers.js └── routes.js ├── license.txt ├── package.json ├── test ├── utilities-test.js └── maths-test.js ├── README.md └── app.js /.eslintignore: -------------------------------------------------------------------------------- 1 | public 2 | templates -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | module.exports = require('./lib/maths.js'); 2 | -------------------------------------------------------------------------------- /templates/withPartials/footer.html: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /templates/withPartials/head.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /templates/helpers/numberformat.js: -------------------------------------------------------------------------------- 1 | // Handlebars helper 2 | // takes a number 3456 and return 3,456 3 | 4 | module.exports = function(number) { 5 | if(number){ 6 | number = number.toString(); 7 | return number.replace(/\B(?=(\d{3})+(?!\d))/g, ","); 8 | }else{ 9 | return number; 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | lib-cov 2 | *.seed 3 | *.log 4 | *.csv 5 | *.dat 6 | *.out 7 | *.pid 8 | *.gz 9 | 10 | data.json 11 | lcov.info 12 | coverage.html 13 | endpoint-test.js 14 | 15 | node_modules 16 | npm-debug.log 17 | .DS_Store 18 | .vscode 19 | .coveralls.yml 20 | .env 21 | 22 | public/swaggerui/static.html 23 | public/swaggerui/static.json -------------------------------------------------------------------------------- /public/images/microsoft.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "rules": { 3 | "indent": [ 4 | 2, 5 | 4 6 | ], 7 | "quotes": [ 8 | 2, 9 | "single" 10 | ], 11 | "linebreak-style": [ 12 | 2, 13 | "unix" 14 | ], 15 | "semi": [ 16 | 2, 17 | "always" 18 | ] 19 | }, 20 | "env": { 21 | "es6": true, 22 | "browser": true 23 | }, 24 | "extends": "eslint:recommended" 25 | } -------------------------------------------------------------------------------- /templates/withPartials/account.html: -------------------------------------------------------------------------------- 1 | {{#if token}} 2 |

{{name}}

3 |

Your API token: {{#if home}} (Its automatically added to the request you make from the forms below) {{/if}} 4 |

{{token}}
5 |

6 | {{#if home}}Account{{else}}API{{/if}} | Sign out 7 | {{else}} 8 |

You need to sign in and get an API token to access some of the APIs functionality.

9 |
10 | 11 |
12 | {{/if}} -------------------------------------------------------------------------------- /lib/utilities.js: -------------------------------------------------------------------------------- 1 | 2 | 'use strict'; 3 | const Boom = require('boom'); 4 | 5 | 6 | module.exports = { 7 | 8 | 9 | generateID: () => { 10 | 11 | return ('0000' + (Math.random() * Math.pow(36, 4) << 0).toString(36)).substr(-4); 12 | }, 13 | 14 | 15 | buildError: (code, error) => { 16 | 17 | return Boom.create(parseInt(code, 10), error); 18 | }, 19 | 20 | 21 | clone: (obj) => { 22 | 23 | return (JSON.parse(JSON.stringify(obj))); 24 | }, 25 | 26 | 27 | isString: (obj) => { 28 | 29 | return typeof (obj) === 'string'; 30 | }, 31 | 32 | 33 | trim: (str) => { 34 | 35 | return str.replace(/^\s+|\s+$/g, ''); 36 | } 37 | 38 | }; 39 | -------------------------------------------------------------------------------- /templates/account.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | MIT License ‐ {{title}} 6 | 7 | 8 | 9 | 10 | {{> head}} 11 | 12 | 13 | 14 | 15 |
16 |
17 |

Account

18 |
19 | 20 |
21 |

Currently sign in as:

22 | {{> account }} 23 | 24 | 25 | {{> footer}} 26 | 27 |
28 | 29 | 30 | -------------------------------------------------------------------------------- /public/images/facebook.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 7 | 10 | 11 | -------------------------------------------------------------------------------- /public/images/instagram.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 6 | 7 | 11 | 12 | -------------------------------------------------------------------------------- /license.txt: -------------------------------------------------------------------------------- 1 | MIT License - hapi-token-docs 2 | 3 | Copyright (c) 2012-2016 Glenn Jones 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 6 | 7 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 8 | 9 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -------------------------------------------------------------------------------- /public/images/twitter.svg: -------------------------------------------------------------------------------- 1 | 2 | 8 | 11 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "hapi-token-docs", 3 | "author": "Glenn Jones", 4 | "version": "1.0.0", 5 | "description": "A example site using HAPI, JWT tokens and swagger documentation", 6 | "keywords": [ 7 | "example", 8 | "calulator", 9 | "hapi", 10 | "jwt", 11 | "token", 12 | "swagger", 13 | "documentation", 14 | "api" 15 | ], 16 | "license": "MIT", 17 | "repository": { 18 | "type": "git", 19 | "url": "http://github.com/glennjones/hapi-token-docs.git" 20 | }, 21 | "scripts": { 22 | "start": "node app.js" 23 | }, 24 | "dependencies": { 25 | "bell": "^8.2.1", 26 | "blipp": "^2.3.0", 27 | "boom": "^4.1.0", 28 | "code": "^4.0.0", 29 | "convert-base": "^0.1.0", 30 | "dotenv": "^2.0.0", 31 | "handlebars": "^4.0.5", 32 | "hapi": "^15.1.1", 33 | "hapi-auth-cookie": "^6.1.1", 34 | "hapi-auth-jwt2": "^5.7.0", 35 | "hapi-require-https": "^2.0.5", 36 | "hapi-swagger": "glennjones/hapi-swagger", 37 | "hoek": "^4.1.0", 38 | "inert": "^3.2.0", 39 | "joi": "9.1.0", 40 | "jsonwebtoken": "^5.7.0", 41 | "lab": "^11.1.0", 42 | "mongoose": "^4.1.0", 43 | "vision": "^4.0.1" 44 | }, 45 | "engines": { 46 | "node": ">= 4.0.x" 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /test/utilities-test.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | const Code = require('code'); 3 | const Lab = require('lab'); 4 | const Utilities = require('../lib/utilities.js'); 5 | 6 | const expect = Code.expect; 7 | const lab = exports.lab = Lab.script(); 8 | 9 | 10 | 11 | lab.experiment('utilities', () => { 12 | 13 | 14 | lab.test('generateID', (done) => { 15 | 16 | //console.log(JSON.stringify(Utilities.generateID())); 17 | expect(Utilities.generateID()).to.exists(); 18 | done(); 19 | }); 20 | 21 | 22 | lab.test('buildError', (done) => { 23 | 24 | //console.log(JSON.stringify(Utilities.buildError( 404, 'text'))); 25 | expect(Utilities.buildError(404, 'text').output.payload).to.deep.equal({ 26 | 'statusCode': 404, 27 | 'error': 'Not Found', 28 | 'message': 'text' 29 | }); 30 | done(); 31 | }); 32 | 33 | 34 | lab.test('clone', (done) => { 35 | 36 | expect(Utilities.clone({ x: 'y' })).to.deep.equal({ x: 'y' }); 37 | done(); 38 | }); 39 | 40 | 41 | lab.test('isString', (done) => { 42 | 43 | expect(Utilities.isString(function () { })).to.equal(false); 44 | expect(Utilities.isString({})).to.equal(false); 45 | expect(Utilities.isString(null)).to.equal(false); 46 | expect(Utilities.isString(undefined)).to.equal(false); 47 | expect(Utilities.isString([])).to.equal(false); 48 | expect(Utilities.isString('string')).to.equal(true); 49 | expect(Utilities.isString(5)).to.equal(false); 50 | done(); 51 | }); 52 | 53 | 54 | lab.test('trim', (done) => { 55 | 56 | expect(Utilities.trim(' text ')).to.equal('text'); 57 | done(); 58 | }); 59 | 60 | 61 | 62 | }); 63 | -------------------------------------------------------------------------------- /templates/license.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | MIT License ‐ {{title}} 6 | 7 | 8 | 9 | 10 | {{> head}} 11 | 12 | 13 | 14 | 15 |
16 |
17 |

MIT License ‐ {{title}}

18 |
19 | 20 |

Copyright (©) 2012-{{year}} Glenn Jones

21 | 22 |

Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:

23 | 24 |

The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.

25 | 26 |

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

27 | 28 | {{> footer}} 29 | 30 |
31 | 32 | 33 | -------------------------------------------------------------------------------- /public/images/linkedin.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /public/images/google.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 7 | 8 | 9 | 19 | 21 | 22 | 23 | 24 | -------------------------------------------------------------------------------- /templates/helpers/jsonstring.js: -------------------------------------------------------------------------------- 1 | // Handlebars helper 2 | // takes a JavaScript object and return a formatted string for display 3 | 4 | 5 | 6 | module.exports = function (context) { 7 | if(context && context !== ''){ 8 | return JSON.stringifyOnce ( context, undefined, 4 ) ; 9 | } 10 | return ''; 11 | } 12 | 13 | 14 | // deals with circle refs 15 | JSON.stringifyOnce = function(obj, replacer, indent){ 16 | var printedObjects = []; 17 | var printedObjectKeys = []; 18 | 19 | function printOnceReplacer(key, value){ 20 | if ( printedObjects.length > 2000){ // browsers will not print more than 2K 21 | return 'object too long'; 22 | } 23 | var printedObjIndex = false; 24 | printedObjects.forEach(function(obj, index){ 25 | if(obj===value){ 26 | printedObjIndex = index; 27 | } 28 | }); 29 | 30 | if ( key == ''){ //root element 31 | printedObjects.push(obj); 32 | printedObjectKeys.push("root"); 33 | return value; 34 | } 35 | 36 | else if(printedObjIndex+"" != "false" && typeof(value)=="object"){ 37 | if ( printedObjectKeys[printedObjIndex] == "root"){ 38 | return "(pointer to root)"; 39 | }else{ 40 | return "(see " + ((!!value && !!value.constructor) ? value.constructor.name.toLowerCase() : typeof(value)) + " with key " + printedObjectKeys[printedObjIndex] + ")"; 41 | } 42 | }else{ 43 | 44 | var qualifiedKey = key || "(empty key)"; 45 | printedObjects.push(value); 46 | printedObjectKeys.push(qualifiedKey); 47 | if(replacer){ 48 | return replacer(key, value); 49 | }else{ 50 | return value; 51 | } 52 | } 53 | } 54 | return JSON.stringify(obj, printOnceReplacer, indent); 55 | }; -------------------------------------------------------------------------------- /public/css/buttons-si-ie8.css: -------------------------------------------------------------------------------- 1 | /* Social Buttons Style */ 2 | .btn-si{ 3 | width: 240px; 4 | height: 60px; 5 | color: #ffffff; 6 | font-size: 16px; 7 | text-decoration: none; 8 | line-height: 38px; 9 | border-radius: 6px; 10 | border: none; 11 | cursor: pointer; 12 | -webkit-transition: all .5s; 13 | transition: all .5s; 14 | } 15 | 16 | .btn-si > img{ 17 | vertical-align: middle; 18 | margin-right: 8px; 19 | } 20 | 21 | .btn-google{ background: #DD4B39; } 22 | .btn-facebook{ background: #3B5998; } 23 | .btn-twitter{ background: #00ACED; } 24 | .btn-microsoft{ background: #e3b30d; } 25 | .btn-github{ background: #2a2a2a; } 26 | .btn-foursquare{ background: #95c330; } 27 | .btn-instagram{ background: #906248; } 28 | .btn-linkedin{ background: #0b5ea3; } 29 | .btn-evernote{ background: #5ca629; } 30 | .btn-dropbox{ background: #1b73d1; } 31 | 32 | .btn-google:hover{ background: #ff5a4a; } 33 | .btn-facebook:hover{ background: #527dd8; } 34 | .btn-twitter:hover{ background: #19c6ff; } 35 | .btn-microsoft:hover{ background: #f2c00c; } 36 | .btn-github:hover{ background: #4e4e4e; } 37 | .btn-foursquare:hover{ background: #a9e031; } 38 | .btn-instagram:hover{ background: #c68562; } 39 | .btn-linkedin:hover{ background: #0c6fc0; } 40 | .btn-evernote:hover{ background: #75d633; } 41 | .btn-dropbox:hover{ background: #1f8dff; } 42 | 43 | .btn-google:active{ background: #b43b2d; } 44 | .btn-facebook:active{ background: #253963; } 45 | .btn-twitter:active{ background: #009eda; } 46 | .btn-microsoft:active{ background: #c89f0a; } 47 | .btn-github:active{ background: #171717; } 48 | .btn-foursquare:active{ background: #6f9321; } 49 | .btn-instagram:active{ background: #5e3f2f; } 50 | .btn-linkedin:active{ background: #06375f; } 51 | .btn-evernote:active{ background: #315a15; } 52 | .btn-dropbox:active{ background: #104a86; } 53 | 54 | /* Hack to fix svg size */ 55 | .btn-si-img-resize{ width: 26px; height: 26px; } 56 | .btn-si-img-resize-bg{ width: 34px; height: 34px; } -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # hapi-token-docs 2 | __Hapi, API boilerplate using tokens__ 3 | 4 | This web API was built to demonstrate some of the [hapi](hapijs.com) features and functionality. I often get asked 5 | how to combine API user logon with token access and swagger documentation. The best answer I have found is to use 6 | a very small amount of customisation for the swagger UI, this can easily be done with `hapi-swagger` plugin. 7 | 8 | __Online Demo: https://hapi-token-docs.herokuapp.com/__ 9 | 10 | API has: 11 | * Github based signin 12 | * Cookie session for API users 13 | * Token based access control to parts of the API 14 | * Auto documentation with swagger UI 15 | * Customised swagger layout 16 | * Token automatically add to user request through swagger 17 | * Redirects to https 18 | 19 | The API is a simple calculator that allows you to add, subtract, divide or multiple two numbers without logging on. To demonstrate a more common set of API calls I also added methods to store sums into a mongodb database to do this you need to loggon and get a token. 20 | 21 | Learn more about the JSON Web Tokens at https://jwt.io/ 22 | 23 | ## Trying out the demo on your local computer 24 | You need have git, node.js and mongodb install on your computer and github account 25 | 26 | 1. From a commandline run `$ git clone https://github.com/glennjones/hapi-token-docs.git` 27 | 2. Move into the project directory `$ cd hapi-token-docs` 28 | 3. Logon to Github and go to https://github.com/settings/developers 29 | 4. Click the "Register new application" use "http://localhost:3033" two url fields 30 | 5. Once created make a note of the "Client ID" and "Client Secret" 31 | 6. Create a new file called `.env` this will store our environment variables 32 | 7. Within the `.env` file add the following lines text changing the values 33 | ``` 34 | PRIVATEKEY=some-text 35 | COOKIE_PASSWORD=some-text-more-than-32-chars 36 | GITHUB_PASSWORD=some-text-more-than-32-chars 37 | GITHUB_CLIENTID=client-id-you-just-got-from-github 38 | GITHUB_CLIENTSECRET=client-secret-you-just-got-from-github 39 | ISSUCURE=false 40 | ``` 41 | 8. Run `$ npm install` 42 | 9. Start the mongodb server `$ mongod` 43 | 10. Run `$ node app` 44 | 11. Connect to the server using `http://localhost:3033` 45 | 46 | ## Lab test 47 | The project has a few unit tests. To run the test within the project type one of the following commands. 48 | ```bash 49 | $ lab 50 | $ lab -r html -o coverage.html 51 | $ lab -r html -o coverage.html --lint 52 | $ lab -r console -o stdout -r html -o coverage.html --lint 53 | ``` 54 | 55 | If you are considering sending a pull request please add tests for the functionality you add or change. 56 | 57 | 58 | ## Issues 59 | If you find any issue please file here on github and I will try and fix them. -------------------------------------------------------------------------------- /public/images/evernote.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Layer 1 5 | 6 | 7 | -------------------------------------------------------------------------------- /public/images/foursquare.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | image/svg+xml -------------------------------------------------------------------------------- /test/maths-test.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | const Code = require('code'); 3 | const Lab = require('lab'); 4 | const maths = require('../lib/maths'); 5 | 6 | const expect = Code.expect; 7 | const lab = exports.lab = Lab.script(); 8 | 9 | 10 | // units tests math.js 11 | 12 | lab.experiment('Math', () => { 13 | 14 | lab.test('should add numbers together', (done) => { 15 | 16 | let options = { 17 | a: 5, 18 | b: 5 19 | }; 20 | maths.add(options, (error, result) => { 21 | 22 | expect(result).to.equal(10); 23 | expect(error).to.equal(null); 24 | done(); 25 | }); 26 | }); 27 | 28 | 29 | lab.test('should capture type errors', (done) => { 30 | 31 | let options = { 32 | a: 'text', 33 | b: 5 34 | }; 35 | maths.add(options, (error, result) => { 36 | 37 | expect(result).to.equal(null); 38 | expect(error).to.equal('The one of the two numbers was not provided'); 39 | done(); 40 | }); 41 | }); 42 | 43 | 44 | lab.test('should capture missing input errors', (done) => { 45 | 46 | let options = { 47 | a: '5' 48 | }; 49 | maths.add(options, (error, result) => { 50 | 51 | expect(result).to.equal(null); 52 | expect(error).to.equal('The one of the two numbers was not provided'); 53 | done(); 54 | }); 55 | }); 56 | 57 | 58 | lab.test('should subtract numbers', (done) => { 59 | 60 | let options = { 61 | a: 10, 62 | b: 5 63 | }; 64 | maths.subtract(options, (error, result) => { 65 | 66 | expect(result).to.equal(5); 67 | expect(error).to.equal(null); 68 | done(); 69 | }); 70 | }); 71 | 72 | 73 | lab.test('should divide numbers', (done) => { 74 | 75 | let options = { 76 | a: 10, 77 | b: 5 78 | }; 79 | maths.divide(options, (error, result) => { 80 | expect(result).to.equal(2); 81 | expect(error).to.equal(null); 82 | done(); 83 | }); 84 | }); 85 | 86 | 87 | lab.test('should divide capture divide by zero errors', (done) => { 88 | 89 | let options = { 90 | a: 10, 91 | b: 0 92 | }; 93 | maths.divide(options, (error, result) => { 94 | 95 | expect(result).to.equal(null); 96 | expect(error).to.equal('One of the supplied numbers is set zero. You cannot divide by zero.'); 97 | done(); 98 | }); 99 | }); 100 | 101 | 102 | lab.test('should multiple numbers', (done) => { 103 | 104 | let options = { 105 | a: 10, 106 | b: 5 107 | }; 108 | maths.multiple(options, (error, result) => { 109 | 110 | expect(result).to.equal(50); 111 | expect(error).to.equal(null); 112 | done(); 113 | }); 114 | }); 115 | 116 | }); 117 | -------------------------------------------------------------------------------- /lib/maths.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | const ConvertBase = require('convert-base'); 3 | const converter = new ConvertBase(); 4 | const internals = {}; 5 | 6 | 7 | module.exports = { 8 | 9 | 10 | // 'add' 11 | add: (options, callback) => { 12 | 13 | let result; 14 | if (internals.validNumbers(options)) { 15 | result = options.a + options.b; 16 | callback(null, internals.toFormat(options, result)); 17 | } else { 18 | callback('The one of the two numbers was not provided', null); 19 | } 20 | }, 21 | 22 | 23 | // 'subtract' 24 | subtract: (options, callback) => { 25 | 26 | let result; 27 | if (internals.validNumbers(options)) { 28 | result = options.a - options.b; 29 | callback(null, internals.toFormat(options, result)); 30 | } else { 31 | callback('The one of the two numbers was not provided', null); 32 | } 33 | }, 34 | 35 | 36 | // 'divide' 37 | divide: (options, callback) => { 38 | 39 | let result; 40 | if (internals.validNumbers(options)) { 41 | if (!internals.isZero(options.a) && !internals.isZero(options.b)) { 42 | result = options.a / options.b; 43 | callback(null, internals.toFormat(options, result)); 44 | } else { 45 | callback('One of the supplied numbers is set zero. You cannot divide by zero.', null); 46 | } 47 | } else { 48 | callback('The one of the two numbers was not provided', null); 49 | } 50 | }, 51 | 52 | 53 | // 'multiple' 54 | multiple: (options, callback) => { 55 | 56 | let result; 57 | if (internals.validNumbers(options)) { 58 | result = options.a * options.b; 59 | callback(null, internals.toFormat(options, result)); 60 | } else { 61 | callback('The one of the two numbers was not provided', null); 62 | } 63 | } 64 | 65 | 66 | }; 67 | 68 | 69 | 70 | // tests that object has properties a and b and they are both numbers 71 | internals.validNumbers = function (options) { 72 | 73 | if (options.hasOwnProperty('a') && 74 | options.hasOwnProperty('b') && 75 | internals.isNumber(options.a) && 76 | internals.isNumber(options.b) 77 | ) { 78 | return true; 79 | } 80 | return false; 81 | }; 82 | 83 | 84 | // if options has a binary format convert number 85 | internals.toFormat = function (options, result) { 86 | 87 | if (options.format === 'binary') { 88 | return converter.convert(result, 10, 2); // decimal to binary 89 | } 90 | return result; 91 | }; 92 | 93 | 94 | // is object a number 95 | internals.isNumber = function (n) { 96 | 97 | return (!isNaN(parseFloat(n)) && isFinite(n)); 98 | }; 99 | 100 | 101 | // is object a number that a 0 (zero) 102 | internals.isZero = function (obj) { 103 | 104 | if (internals.isNumber(obj) && obj === 0) { 105 | return true; 106 | } 107 | return false; 108 | }; 109 | -------------------------------------------------------------------------------- /public/css/buttons-si.css: -------------------------------------------------------------------------------- 1 | /* Social Buttons Style */ 2 | .btn-si { 3 | background-position: 1em; 4 | background-repeat: no-repeat; 5 | background-size: 2em; 6 | border-radius: 0.5em; 7 | border: none; 8 | color: white; 9 | cursor: pointer; 10 | font-size: 1em; 11 | height: 4em; 12 | line-height: 1em; 13 | padding: 0 2em 0 4em; 14 | text-decoration: none; 15 | transition: all 0.5s; } 16 | 17 | .btn-google { 18 | background-color: #dd4b39; 19 | background-image: url("../images/google.svg"); } 20 | .btn-google:hover { 21 | background-color: #e47365; } 22 | .btn-google:active { 23 | background-color: #c23321; } 24 | 25 | .btn-facebook { 26 | background-color: #3b5998; 27 | background-image: url("../images/facebook.svg"); } 28 | .btn-facebook:hover { 29 | background-color: #4c70ba; } 30 | .btn-facebook:active { 31 | background-color: #2d4373; } 32 | 33 | .btn-twitter { 34 | background-color: #00aced; 35 | background-image: url("../images/twitter.svg"); } 36 | .btn-twitter:hover { 37 | background-color: #21c2ff; } 38 | .btn-twitter:active { 39 | background-color: #0087ba; } 40 | 41 | .btn-microsoft { 42 | background-color: #e3b30d; 43 | background-image: url("../images/microsoft.svg"); } 44 | .btn-microsoft:hover { 45 | background-color: #f3c730; } 46 | .btn-microsoft:active { 47 | background-color: #b38d0a; } 48 | 49 | .btn-github { 50 | background-color: #2a2a2a; 51 | background-image: url("github.svg"); } 52 | .btn-github:hover { 53 | background-color: #444444; } 54 | .btn-github:active { 55 | background-color: #101010; } 56 | 57 | .btn-foursquare { 58 | background-color: #95c330; 59 | background-image: url("../images/foursquare.svg"); } 60 | .btn-foursquare:hover { 61 | background-color: #abd452; } 62 | .btn-foursquare:active { 63 | background-color: #769a26; } 64 | 65 | .btn-instagram { 66 | background-color: #906248; 67 | background-image: url("../images/instagram.svg"); } 68 | .btn-instagram:hover { 69 | background-color: #ae7a5d; } 70 | .btn-instagram:active { 71 | background-color: #6e4b37; } 72 | 73 | .btn-linkedin { 74 | background-color: #0b5ea3; 75 | background-image: url("../images/linkedin.svg"); } 76 | .btn-linkedin:hover { 77 | background-color: #0e7ad3; } 78 | .btn-linkedin:active { 79 | background-color: #084273; } 80 | 81 | .btn-evernote { 82 | background-color: #5ca629; 83 | background-image: url("../images/evernote.svg"); } 84 | .btn-evernote:hover { 85 | background-color: #73cd35; } 86 | .btn-evernote:active { 87 | background-color: #457d1f; } 88 | 89 | .btn-dropbox { 90 | background-color: #1b73d1; 91 | background-image: url("../images/dropbox.svg"); } 92 | .btn-dropbox:hover { 93 | background-color: #3a8de5; } 94 | .btn-dropbox:active { 95 | background-color: #155aa4; } 96 | 97 | .btn-bitbucket { 98 | background-color: #205081; 99 | background-image: url("../images/bitbucket.svg"); } 100 | .btn-bitbucket:hover { 101 | background-color: #3572B0; } 102 | .btn-bitbucket:active { 103 | background-color: ##4E8AC7; } 104 | 105 | .btn-si-a { 106 | padding: 25px 15px 25px 65px !important; 107 | font-family: arial; } 108 | 109 | .smaller .btn-si-a { 110 | padding-left: 40px !important; 111 | font-size: 12px; } 112 | -------------------------------------------------------------------------------- /public/images/bitbucket.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | -------------------------------------------------------------------------------- /public/css/examples.css: -------------------------------------------------------------------------------- 1 | /* Glenn Jones - taken from ident engine */ 2 | 3 | .str { color: #85C5DC; } 4 | .kwd { color: #EDF0D1; } 5 | .com { color: #878989; } 6 | .typ { color: #F5896F; } 7 | .lit { color: #FFB17A; } 8 | .pun { color: #FFFFFF; } 9 | .pln { color: #FFFFFF; } 10 | .tag { color: #F5896F; } 11 | .atn { color: #F5896F; } 12 | .atv { color: #85C5DC; } 13 | .dec { color: #878989; } 14 | 15 | pre.prettyprint { 16 | background-color:#302F2D; 17 | border: none; 18 | line-height: normal; 19 | font-size: 100%; 20 | border-radius: 6px 6px 6px 6px; 21 | font-family: consolas,​'andale mono',​'courier new',​monospace; 22 | padding-top: 12px; 23 | overflow: hidden; 24 | } 25 | 26 | code{ 27 | font-size: 13px; 28 | line-height: normal; 29 | } 30 | 31 | /* Specify class=linenums on a pre to get line numbering */ 32 | ol.linenums { margin-top: 0; margin-bottom: 0 } /* IE indents via margin-left */ 33 | li.L0, 34 | li.L1, 35 | li.L2, 36 | li.L3, 37 | li.L5, 38 | li.L6, 39 | li.L7, 40 | li.L8 { list-style-type: none } 41 | /* Alternate shading for lines */ 42 | li.L1, 43 | li.L3, 44 | li.L5, 45 | li.L7, 46 | li.L9 { background: #eee } 47 | 48 | @media print { 49 | .str { color: #060; } 50 | .kwd { color: #006; font-weight: bold; } 51 | .com { color: #600; font-style: italic; } 52 | .typ { color: #404; font-weight: bold; } 53 | .lit { color: #044; } 54 | .pun { color: #440; } 55 | .pln { color: #000; } 56 | .tag { color: #006; font-weight: bold; } 57 | .atn { color: #404; } 58 | .atv { color: #060; } 59 | } 60 | 61 | 62 | header, section, footer { 63 | width: 100%; 64 | float: none; 65 | position: relative; 66 | } 67 | 68 | 69 | h1 { 70 | margin-top: 0; 71 | } 72 | 73 | .home-link{ 74 | margin: 0; 75 | } 76 | 77 | 78 | .services{ 79 | margin-top: 2em; 80 | list-style: none; 81 | margin-bottom: 60px; 82 | } 83 | 84 | .services li{ 85 | height: 24px; 86 | border-bottom: solid 1px #ccc; 87 | margin-bottom: 2px; 88 | background-size: 16px 16px; 89 | width: 100%; 90 | font-size: 14px; 91 | } 92 | 93 | .services li span{ 94 | display: inline-block; 95 | margin-right: 10px; 96 | color: #666; 97 | } 98 | 99 | .services li a{ 100 | 101 | } 102 | 103 | #form { 104 | margin-bottom: 20px; 105 | } 106 | 107 | .gray-button { 108 | background: #e5e5e5; 109 | background: -moz-linear-gradient(top, #e5e5e5 0%, #999999 100%); 110 | background: -webkit-gradient(linear, left top, left bottom, color-stop(0%,#e5e5e5), color-stop(100%,#999999)); 111 | background: -webkit-linear-gradient(top, #e5e5e5 0%,#999999 100%); 112 | background: -o-linear-gradient(top, #e5e5e5 0%,#999999 100%); 113 | background: -ms-linear-gradient(top, #e5e5e5 0%,#999999 100%); 114 | background: linear-gradient(to bottom, #e5e5e5 0%,#999999 100%); 115 | filter: progid:DXImageTransform.Microsoft.gradient( startColorstr='#e5e5e5', endColorstr='#999999',GradientType=0 ); 116 | border-color: #ababab #999999 #8c8c8c; 117 | border-style: solid; 118 | border-width: 1px; 119 | box-shadow: 0 1px 1px rgba(0, 0, 0, 0.3), 0 1px 0 #f2f2f2 inset; 120 | } 121 | 122 | .gray-button:active, .button-down { 123 | background: #999999; 124 | background: -moz-linear-gradient(top, #999999 0%, #e5e5e5 100%); 125 | background: -webkit-gradient(linear, left top, left bottom, color-stop(0%,#999999), color-stop(100%,#e5e5e5)); 126 | background: -webkit-linear-gradient(top, #999999 0%,#e5e5e5 100%); 127 | background: -o-linear-gradient(top, #999999 0%,#e5e5e5 100%); 128 | background: -ms-linear-gradient(top, #999999 0%,#e5e5e5 100%); 129 | background: linear-gradient(to bottom, #999999 0%,#e5e5e5 100%); 130 | filter: progid:DXImageTransform.Microsoft.gradient( startColorstr='#999999', endColorstr='#e5e5e5',GradientType=0 ); 131 | } 132 | 133 | input[type="submit"].inline-button { 134 | margin: 0; 135 | display: inline-block; 136 | padding: 2px 8px; 137 | float: right; 138 | } 139 | 140 | footer{ 141 | margin-top: 10em; 142 | } 143 | 144 | footer p{ 145 | margin: 0; 146 | } 147 | 148 | 149 | 150 | 151 | -------------------------------------------------------------------------------- /public/css/github.svg: -------------------------------------------------------------------------------- 1 | 2 | image/svg+xml 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | -------------------------------------------------------------------------------- /public/images/github.svg: -------------------------------------------------------------------------------- 1 | 2 | image/svg+xml 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | -------------------------------------------------------------------------------- /app.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | require('dotenv').config(); 3 | const Hapi = require('hapi'); 4 | const Inert = require('inert'); 5 | const Vision = require('vision'); 6 | const Blipp = require('blipp'); 7 | const Hoek = require('hoek'); 8 | const RequireHttps = require('hapi-require-https'); 9 | const HapiJWT2 = require('hapi-auth-jwt2'); 10 | const Bell = require('bell'); 11 | const AuthCookie = require('hapi-auth-cookie'); 12 | const Pack = require('./package'); 13 | const Routes = require('./lib/routes.js'); 14 | 15 | 16 | const appendAuthMessages = function (routes) { 17 | 18 | routes.forEach((route) => { 19 | if (Hoek.reach(route, 'config.tags')) { 20 | const tags = Hoek.reach(route, 'config.tags'); 21 | if (tags.indexOf('api') > -1) { 22 | 23 | if (!route.config.notes) { 24 | route.config.notes = []; 25 | } 26 | 27 | if (route.config.auth === 'jwt') { 28 | route.config.notes.push('__Token is required to access this endpoint. Make sure you are signed in.__'); 29 | } 30 | } 31 | } 32 | }); 33 | }; 34 | 35 | appendAuthMessages(Routes); 36 | 37 | 38 | const privateKey = process.env.PRIVATEKEY; 39 | const info = { 40 | version: Pack.version, 41 | title: 'Hapi - token - docs', 42 | description: 'This web API was built to demonstrate some of the hapi features and functionality.' 43 | }; 44 | 45 | 46 | const server = new Hapi.Server({ 47 | app: { 48 | 'privateKey': privateKey, 49 | 'info': info 50 | } 51 | }); 52 | server.connection({ 53 | host: (process.env.PORT) ? '0.0.0.0' : 'localhost', 54 | port: (process.env.PORT || 3033), 55 | routes: { cors: true } 56 | }); 57 | 58 | 59 | // setup swagger options 60 | const swaggerOptions = { 61 | info: info, 62 | tags: [{ 63 | 'name': 'sum', 64 | 'description': 'Working with maths', 65 | 'externalDocs': { 66 | 'description': 'Find out more', 67 | 'url': 'http://example.org' 68 | } 69 | }, { 70 | 'name': 'store', 71 | 'description': 'Storing your sums for later use', 72 | 'externalDocs': { 73 | 'description': 'Find out more', 74 | 'url': 'http://example.org' 75 | } 76 | }], 77 | securityDefinitions: { 78 | 'jwt': { 79 | 'type': 'apiKey', 80 | 'name': 'Authorization', 81 | 'in': 'header' 82 | } 83 | }, 84 | security: [{ 'jwt': [] }], 85 | auth: false 86 | }; 87 | 88 | 89 | // validation function for JWT token 90 | var validate = function (decoded, request, callback) { 91 | 92 | // add yours checks here i.e. is the user valid 93 | if (decoded.id && decoded.name) { 94 | return callback(null, true, decoded); 95 | } 96 | return callback(null, false); 97 | 98 | }; 99 | 100 | 101 | // register auth plugins and strategies before anything else 102 | server.register([ 103 | RequireHttps, 104 | AuthCookie, 105 | Bell, 106 | HapiJWT2 107 | ], function (err) { 108 | 109 | if (err) { 110 | console.log(err); 111 | } 112 | 113 | server.auth.strategy('hapi-token-docs-cookie', 'cookie', { 114 | password: process.env.COOKIE_PASSWORD, 115 | cookie: 'hapi-token-docs', 116 | isSecure: process.env.ISSUCURE 117 | }); 118 | 119 | server.auth.strategy('github-oauth', 'bell', { 120 | provider: 'github', 121 | password: process.env.GITHUB_PASSWORD, 122 | clientId: process.env.GITHUB_CLIENTID, 123 | clientSecret: process.env.GITHUB_CLIENTSECRET, 124 | isSecure: process.env.ISSUCURE, 125 | forceHttps: process.env.ISSUCURE 126 | }); 127 | 128 | server.auth.strategy('jwt', 'jwt', { 129 | key: privateKey, 130 | validateFunc: validate, 131 | verifyOptions: { algorithms: ['HS256'] } 132 | }); 133 | 134 | server.auth.default('hapi-token-docs-cookie'); 135 | }); 136 | 137 | 138 | 139 | // register other plugins and start 140 | server.register([ 141 | Inert, 142 | Vision, 143 | { 144 | register: Blipp, 145 | options: { showAuth: true } 146 | }, 147 | { 148 | register: require('hapi-swagger'), 149 | options: swaggerOptions 150 | } 151 | ], function (err) { 152 | 153 | if (err) { 154 | console.log(err); 155 | } 156 | 157 | // add routes 158 | server.route(Routes); 159 | 160 | server.start(function () { 161 | 162 | console.log('Server running at:', server.info.uri); 163 | }); 164 | }); 165 | 166 | 167 | 168 | // add templates support with handlebars 169 | server.views({ 170 | path: 'templates', 171 | engines: { html: require('handlebars') }, 172 | partialsPath: './templates/withPartials', 173 | helpersPath: './templates/helpers', 174 | isCached: false 175 | }); 176 | -------------------------------------------------------------------------------- /lib/store.js: -------------------------------------------------------------------------------- 1 | 2 | 'use strict'; 3 | // data access layer 4 | const Utils = require('../lib/utilities.js'); 5 | 6 | 7 | const internals = {}; 8 | 9 | 10 | const Store = function (mongoose, connection, options) { 11 | 12 | this.options = options; 13 | this.Schema = mongoose.Schema; 14 | this.ObjectId = this.Schema.ObjectId; 15 | 16 | this.StoreSchema = new this.Schema({ 17 | 'dbid': this.ObjectId, 18 | 'id': String, 19 | 'a': Number, 20 | 'b': Number, 21 | 'operator': String, 22 | 'equals': Number, 23 | 'created': { type: Date, index: true }, 24 | 'modified': { type: Date, index: true } 25 | }); 26 | 27 | this.model = connection.model('store', this.StoreSchema); 28 | }; 29 | module.exports = Store; 30 | 31 | 32 | Store.prototype = { 33 | 34 | // adds a new item 35 | add: function (item, callback) { 36 | 37 | item.id = Utils.generateID(); 38 | item.created = new Date(); 39 | 40 | new this.model(item).save( (err, doc) => { 41 | 42 | if (err){ 43 | console.log(err); 44 | } 45 | if (callback) { 46 | if (doc) { 47 | // quick way to simplify object 48 | doc = Utils.clone(doc); 49 | delete doc._id; 50 | delete doc.__v; 51 | } 52 | callback(err, doc); 53 | } 54 | }); 55 | }, 56 | 57 | 58 | // append a new item or save an existing one 59 | update: function (item, callback) { 60 | 61 | this.get({ id: item.id, dataType: 'internal' }, (err, doc) => { 62 | 63 | if (doc) { 64 | // update 65 | doc.a = item.a; 66 | doc.b = item.b; 67 | doc.operator = item.operator; 68 | doc.equals = item.equals; 69 | doc.modified = new Date(); 70 | 71 | doc.save( (err, savedDoc) =>{ 72 | 73 | if (savedDoc) { 74 | // quick way to simplify object 75 | savedDoc = Utils.clone(savedDoc); 76 | delete savedDoc._id; 77 | delete savedDoc.__v; 78 | } 79 | callback(err, savedDoc); 80 | }); 81 | } else { 82 | callback(Utils.buildError('404', 'Sum not found'), null); 83 | } 84 | }); 85 | }, 86 | 87 | 88 | // get a single item 89 | get: function (options, callback) { 90 | 91 | if (options.id) { 92 | this.model.findOne({ 'id': options.id }, (err, doc) => { 93 | 94 | if (doc) { 95 | // dataType is internal or json the default is json 96 | if (!options.dataType || options.dataType === 'json') { 97 | // quick way to simplify object 98 | doc = Utils.clone(doc); 99 | delete doc._id; 100 | delete doc.__v; 101 | } 102 | callback(null, doc); 103 | } else { 104 | callback(Utils.buildError('404', 'Sum not found'), null); 105 | } 106 | }); 107 | } else { 108 | callback(Utils.buildError('400', 'No sum id passed to find item'), null); 109 | } 110 | }, 111 | 112 | 113 | // paging query - keep number of object requested low or use mongoosejs stream 114 | list: function (options, callback) { 115 | 116 | let skipFrom = (options.page * options.pageSize) - options.pageSize; 117 | let model = this.model; 118 | 119 | if (!options.sort) { 120 | options.sort = { modified: 1 }; 121 | } 122 | 123 | if (!options.query) { 124 | options.query = {}; 125 | } 126 | 127 | model.find(options.query) 128 | .skip(skipFrom) 129 | .limit(options.pageSize) 130 | .sort(options.sort) 131 | .exec( (err, data) => { 132 | 133 | if (err) { 134 | callback({ 'error': err }); 135 | } else { 136 | model.count(options.query, (err, count) => { 137 | 138 | if (err) { 139 | callback(err, null); 140 | } else { 141 | let i = data.length; 142 | while (i--) { 143 | delete data[i]._doc._id; 144 | delete data[i]._doc.__v; 145 | } 146 | // quick way to simplify object 147 | data = Utils.clone(data); 148 | 149 | callback(null, { 150 | 'items': data, 151 | 'count': count, 152 | 'pageSize': options.pageSize, 153 | 'page': options.page, 154 | 'pageCount': Math.ceil(count / options.pageSize) 155 | }); 156 | } 157 | }); 158 | } 159 | }); 160 | }, 161 | 162 | 163 | // remove documents from db collection using a query 164 | remove: function (options, callback) { 165 | 166 | const self = this; 167 | if (options.id) { 168 | this.get(options, (err, data) => { 169 | 170 | if (data) { 171 | self.model.remove({ 'id': options.id }, (err) => { 172 | 173 | callback(err, null); 174 | }); 175 | } else { 176 | callback(err, null); 177 | } 178 | }); 179 | } else { 180 | callback(Utils.buildError('400', 'No sum id passed to find item'), null); 181 | } 182 | }, 183 | 184 | 185 | // remove all documents from db collection 186 | removeAll: function () { 187 | this.model.remove({}, (err) => { 188 | 189 | console.log(err); 190 | }); 191 | } 192 | 193 | }; 194 | -------------------------------------------------------------------------------- /templates/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | HAPI - Token - Docs 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 141 | 142 | 143 | 144 | 145 | {{> head}} 146 | 147 | 153 | 154 | 155 | 156 | 157 | 158 | 159 | 160 | 161 |
162 |
163 | {{#if hapiSwagger.info.title}} 164 |

{{hapiSwagger.info.title}}

165 | {{else}} 166 |

{{{title}}}

167 | {{/if}} 168 |
169 | 170 | 171 | 172 | {{#if hapiSwagger.info.description}} 173 |

{{hapiSwagger.info.description}} Code for project: https://github.com/glennjones/hapi-token-docs

174 | {{/if}} 175 | 176 | 177 | 178 |
179 | 180 |
181 | 182 |
183 | 184 |

API Access

185 | {{> account home=true}} 186 |
187 | 188 | 189 | 190 |
191 |

API

192 |
193 |
194 | 195 | 196 | {{#if hapiSwagger.info.license}} 197 |
198 | License: 199 | {{#if hapiSwagger.info.licenseUrl}}{{/if}} 200 | {{hapiSwagger.info.license}} 201 | {{#if hapiSwagger.info.licenseUrl}}{{/if}} 202 |
203 | {{/if}} 204 | 205 | {{> footer}} 206 | 207 |
208 | 209 | 210 | -------------------------------------------------------------------------------- /lib/handlers.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | const Boom = require('boom'); 3 | const Joi = require('joi'); 4 | const Mongoose = require('mongoose'); 5 | const JWT = require('jsonwebtoken'); 6 | const Maths = require('../lib/maths.js'); 7 | const Store = require('../lib/store.js'); 8 | const Utils = require('../lib/utilities.js'); 9 | 10 | 11 | const internals = {}; 12 | // moungodb connection for store 13 | const adminId = 175288; // github user id for glennjones 14 | const norights = Boom.unauthorized('You need admin rights to add, remove and update a stored item'); 15 | const mongodbURL = (process.env.MONGOLAB_URI || 'mongodb://127.0.0.1/be-more-hapi'); 16 | const connection = Mongoose.createConnection(mongodbURL); 17 | const store = new Store(Mongoose, connection); 18 | 19 | Mongoose.connection.on('error', (err) => { 20 | console.log(['error'], 'moungodb connect error: ' + err); 21 | }); 22 | 23 | 24 | 25 | module.exports = { 26 | 27 | 28 | index: function (request, reply) { 29 | 30 | let context = { title: 'HAPI - Token - Docs' }; 31 | if (request.auth.isAuthenticated) { 32 | context.isAuthenticated = true; 33 | context.name = request.auth.credentials.profile.displayName; 34 | context.token = request.auth.credentials.token; 35 | } 36 | reply.view('index.html', context); 37 | }, 38 | 39 | 40 | login: function (request, reply) { 41 | 42 | if (request.auth.isAuthenticated) { 43 | 44 | // get data from github 45 | const privateKey = request.server.settings.app.privateKey; 46 | const id = request.auth.credentials.profile.id; 47 | const name = request.auth.credentials.profile.displayName; 48 | 49 | // create token with github data 50 | request.auth.credentials.token = JWT.sign({ 51 | id: id, 52 | name: name 53 | }, privateKey, { algorithm: 'HS256' }); 54 | 55 | // set cookie and redirect to docs 56 | request.cookieAuth.set(request.auth.credentials); 57 | reply.redirect('/'); 58 | } else { 59 | // reply with error 60 | reply('Not logged in...').code(401); 61 | } 62 | }, 63 | 64 | 65 | logout: function (request, reply) { 66 | 67 | request.cookieAuth.clear(); 68 | reply.redirect('/'); 69 | }, 70 | 71 | 72 | account: function (request, reply) { 73 | 74 | let context = { title: request.server.settings.app.info.title }; 75 | if (request.auth.isAuthenticated) { 76 | context.id = request.auth.credentials.profile.id; 77 | context.name = request.auth.credentials.profile.displayName; 78 | context.token = request.auth.credentials.token; 79 | } 80 | reply.view('account.html', context); 81 | }, 82 | 83 | 84 | license: function (request, reply) { 85 | 86 | reply.view('license.html', { 87 | title: request.server.settings.app.info.title, 88 | year: new Date().getFullYear() 89 | }); 90 | }, 91 | 92 | 93 | // Sums 94 | /* ----------------------------------------------------------------------------------- */ 95 | 96 | add: function (request, reply) { 97 | 98 | Maths.add(internals.buildOptions(request), (error, result) => { 99 | 100 | internals.renderJSON(request, reply, error, { 'equals': parseFloat(result) }); 101 | }); 102 | }, 103 | 104 | 105 | subtract: function (request, reply) { 106 | 107 | Maths.subtract(internals.buildOptions(request), (error, result) => { 108 | 109 | internals.renderJSON(request, reply, error, { 'equals': parseFloat(result) }); 110 | }); 111 | }, 112 | 113 | 114 | divide: function (request, reply) { 115 | 116 | Maths.divide(internals.buildOptions(request), (error, result) => { 117 | 118 | internals.renderJSON(request, reply, error, { 'equals': parseFloat(result) }); 119 | }); 120 | }, 121 | 122 | 123 | multiple: function (request, reply) { 124 | 125 | Maths.multiple(internals.buildOptions(request), (error, result) => { 126 | 127 | internals.renderJSON(request, reply, error, { 'equals': parseFloat(result) }); 128 | }); 129 | }, 130 | 131 | 132 | // Store 133 | /* ----------------------------------------------------------------------------------- */ 134 | 135 | storeList: function (request, reply) { 136 | 137 | let options = { 138 | query: {} 139 | }; 140 | // add defaults as these querystring are optional 141 | options.page = (request.query.page) ? parseInt(request.query.page, 10) : 1; 142 | options.pageSize = (request.query.pagesize) ? parseInt(request.query.pagesize, 10) : 20; 143 | 144 | store.list(options, (error, result) => { 145 | 146 | internals.renderJSON(request, reply, error, result); 147 | }); 148 | }, 149 | 150 | 151 | storeItem: function (request, reply) { 152 | 153 | let options = { 154 | id: request.params.id 155 | }; 156 | 157 | store.get(options, (error, result) => { 158 | if (result) { 159 | result = Utils.clone(result); 160 | } 161 | internals.renderJSON(request, reply, error, result); 162 | }); 163 | }, 164 | 165 | 166 | storeAdd: function (request, reply) { 167 | 168 | if (internals.checkAdminScope(request)) { 169 | let options = request.payload; 170 | if (request.query.a) { 171 | options = { 172 | a: parseFloat(request.query.a), 173 | b: parseFloat(request.query.b), 174 | operator: request.query.operator, 175 | equals: parseFloat(request.query.equals) 176 | }; 177 | } 178 | store.add(options, (error, result) => { 179 | 180 | internals.renderJSON(request, reply, error, result); 181 | }); 182 | } else { 183 | reply( norights ); 184 | } 185 | }, 186 | 187 | 188 | storeAddFile: function (request, reply) { 189 | 190 | if (internals.checkAdminScope(request)) { 191 | let payload = request.payload; 192 | let data = ''; 193 | 194 | console.log(payload.file[0]); 195 | 196 | // check that required file is present 197 | // the filepath property incorrectlty uses a string 'undefined' 198 | if (payload.file && payload.file[0] !== 'undefined') { 199 | 200 | var file = payload.file[1], 201 | headers = file.hapi.headers; 202 | 203 | // check the content-type is json 204 | if (headers['content-type'] === 'application/json') { 205 | 206 | // read the stream 207 | file.on('data', (chunk) => { 208 | 209 | data += chunk; 210 | }); 211 | 212 | // once we have all the data 213 | file.on('end', () => { 214 | 215 | // use Joi to validate file data format 216 | var addSumSchema = Joi.object().keys({ 217 | a: Joi.number().required(), 218 | b: Joi.number().required(), 219 | operator: Joi.string().required(), 220 | equals: Joi.number().required() 221 | }); 222 | 223 | Joi.validate(data, addSumSchema, (err, value) => { 224 | 225 | if (err) { 226 | reply(Boom.badRequest('JSON file has incorrect format or properties. ' + err)); 227 | } else { 228 | store.add(JSON.parse(data), (error, result) => { 229 | 230 | internals.renderJSON(request, reply, error, result); 231 | }); 232 | } 233 | }); 234 | }); 235 | 236 | } else { 237 | reply(Boom.unsupportedMediaType()); 238 | } 239 | 240 | } else { 241 | reply(Boom.badRequest('File is required')); 242 | } 243 | } else { 244 | reply( norights ); 245 | } 246 | }, 247 | 248 | 249 | storeUpdate: function (request, reply) { 250 | 251 | if (internals.checkAdminScope(request)) { 252 | let options = { 253 | id: request.params.id, 254 | a: parseFloat(request.payload.a), 255 | b: parseFloat(request.payload.b), 256 | operator: request.payload.operator, 257 | equals: parseFloat(request.payload.equals) 258 | }; 259 | 260 | store.update(options, (error, result) => { 261 | internals.renderJSON(request, reply, error, result); 262 | }); 263 | } else { 264 | reply( norights ); 265 | } 266 | }, 267 | 268 | 269 | storeRemove: function (request, reply) { 270 | 271 | if (internals.checkAdminScope(request)) { 272 | let options = { 273 | id: request.params.id 274 | }; 275 | store.remove(options, (error, result) => { 276 | 277 | internals.renderJSON(request, reply, error, result); 278 | }); 279 | } else { 280 | reply( norights ); 281 | } 282 | } 283 | }; 284 | 285 | 286 | // create options object from request 287 | internals.buildOptions = function (request) { 288 | 289 | let options = { 290 | a: 0, 291 | b: 0, 292 | format: 'decimal' 293 | }; 294 | 295 | if (request.headers && request.headers['x-format']) { 296 | options.format = request.headers['x-format']; 297 | } 298 | if (request.query && request.query.a) { 299 | // querystring 300 | options.a = parseFloat(request.query.a); 301 | options.b = parseFloat(request.query.b); 302 | } else if (request.params && request.params.a) { 303 | // url params or fragment 304 | options.a = parseFloat(request.params.a); 305 | options.b = parseFloat(request.params.b); 306 | } 307 | return options; 308 | }; 309 | 310 | 311 | // render json out to http stream 312 | internals.renderJSON = function (request, reply, error, result) { 313 | 314 | if (error) { 315 | if (Utils.isString(error)) { 316 | reply(Utils.buildError(400, error)); 317 | } else { 318 | reply(error); 319 | } 320 | } else { 321 | reply(result).type('application/json; charset=utf-8'); 322 | } 323 | }; 324 | 325 | 326 | internals.checkAdminScope = function (request) { 327 | 328 | const id = request.auth.credentials.id; 329 | return (id === adminId); 330 | 331 | }; 332 | 333 | 334 | 335 | 336 | 337 | 338 | 339 | 340 | -------------------------------------------------------------------------------- /public/css/styles.css: -------------------------------------------------------------------------------- 1 | /* Glenn Jones - taken from ident engine */ 2 | 3 | @import url(https://fonts.googleapis.com/css?family=Lato:300italic,700italic,300,700); 4 | 5 | body { 6 | padding:2em; 7 | padding-top: 0; 8 | margin: 0; 9 | font:14px/1.5 Lato, "Helvetica Neue", Helvetica, Arial, sans-serif; 10 | color:#333; 11 | font-weight:300; 12 | font-size: 100%; 13 | } 14 | 15 | h1, h2, h3, h4, h5, h6 { 16 | color:#222; 17 | margin:0 0 20px; 18 | font-family: Lato, "Helvetica Neue", Helvetica, Arial, sans-serif; 19 | font-weight:300; 20 | } 21 | 22 | p, ul, ol, table, pre, dl { 23 | margin:0 0 20px; 24 | } 25 | 26 | h1{ 27 | font-size: 1.5em; 28 | } 29 | 30 | h2, h3 { 31 | margin-top: 3em; 32 | margin-bottom: 0.8em; 33 | font-size: 1.17em; 34 | } 35 | 36 | h1, h2, h3 { 37 | line-height:1.1; 38 | clear: both; 39 | text-transform: uppercase; 40 | font-weight: normal; 41 | } 42 | 43 | 44 | ol{ 45 | list-style-type: lower-alpha; 46 | } 47 | 48 | ol li{ 49 | margin-bottom: 1em; 50 | } 51 | 52 | h1.home-link{ 53 | font-size: 1.8em; 54 | font-weight: 400; 55 | margin-top: 4em; 56 | margin-bottom: 0.4em; 57 | 58 | text-transform: uppercase; 59 | padding: 0; 60 | position: relative; 61 | } 62 | 63 | h1.home-link a{ 64 | color: #333; 65 | display: block; 66 | position: absolute; bottom: 0; left: 0; 67 | } 68 | 69 | h1.entry-title{ 70 | margin-top: 2em; 71 | margin-bottom: 1em; 72 | } 73 | 74 | 75 | 76 | header h2 { 77 | color:#ff6666; 78 | } 79 | 80 | h3, h4, h5, h6 { 81 | color:#494949; 82 | } 83 | 84 | a { 85 | color:#39c; 86 | text-decoration:none; 87 | } 88 | 89 | a:hover { 90 | text-decoration:underline; 91 | } 92 | 93 | a small { 94 | font-size:11px; 95 | color:#777; 96 | margin-top:-0.6em; 97 | display:block; 98 | } 99 | 100 | .wrapper { 101 | width:800px; 102 | margin:0 auto; 103 | } 104 | 105 | blockquote { 106 | border-left:1px solid #e5e5e5; 107 | margin:0; 108 | padding:0 0 0 20px; 109 | font-style:italic; 110 | } 111 | 112 | code, pre { 113 | font-family:Monaco, Bitstream Vera Sans Mono, Lucida Console, Terminal; 114 | color:#333; 115 | font-size:12px; 116 | } 117 | 118 | pre { 119 | padding:8px 15px; 120 | background: #F0F0F0; 121 | border-radius:5px; 122 | border:1px solid #e5e5e5; 123 | overflow-x: auto; 124 | } 125 | 126 | table { 127 | width:100%; 128 | border-collapse:collapse; 129 | } 130 | 131 | th, td { 132 | text-align:left; 133 | padding:5px 10px; 134 | border-bottom:1px solid #e5e5e5; 135 | } 136 | 137 | dt { 138 | color:#444; 139 | font-weight:700; 140 | } 141 | 142 | th { 143 | color:#444; 144 | } 145 | 146 | img { 147 | max-width:100%; 148 | } 149 | 150 | nav{ 151 | border-top: 1px solid #e7e7e7; 152 | clear: both; 153 | padding-top: 0.5em; 154 | } 155 | 156 | .menu { 157 | list-style-image: none; 158 | list-style-position: outside; 159 | list-style-type: none; 160 | margin: 0 auto; 161 | padding: 0; 162 | font-weight: normal; 163 | } 164 | 165 | .menu li { 166 | display: block; 167 | float: left; 168 | margin: 0; 169 | padding: 0; 170 | } 171 | 172 | .menu li a { 173 | border-right: 1px solid #eee; 174 | display: block; 175 | padding-left: 1em; 176 | padding-right: 1em; 177 | text-decoration: none; 178 | text-transform: lowercase; 179 | color: #000; 180 | } 181 | 182 | .menu li a:hover { 183 | text-decoration:underline; 184 | } 185 | 186 | .menu .lastItem{ 187 | border: none; 188 | } 189 | 190 | .menu .firstItem{ 191 | padding-left: 0; 192 | } 193 | 194 | dl{ 195 | margin-left: 1.4em; 196 | } 197 | 198 | dd{ 199 | margin-left: 0; 200 | margin-bottom: 1em; 201 | } 202 | 203 | 204 | header { 205 | width:270px; 206 | float:left; 207 | position:fixed; 208 | } 209 | 210 | header .buttons { 211 | list-style:none; 212 | height:40px; 213 | 214 | padding:0; 215 | 216 | background: #eee; 217 | background: -moz-linear-gradient(top, #f8f8f8 0%, #dddddd 100%); 218 | background: -webkit-gradient(linear, left top, left bottom, color-stop(0%,#f8f8f8), color-stop(100%,#dddddd)); 219 | background: -webkit-linear-gradient(top, #f8f8f8 0%,#dddddd 100%); 220 | background: -o-linear-gradient(top, #f8f8f8 0%,#dddddd 100%); 221 | background: -ms-linear-gradient(top, #f8f8f8 0%,#dddddd 100%); 222 | background: linear-gradient(top, #f8f8f8 0%,#dddddd 100%); 223 | 224 | border-radius:5px; 225 | border:1px solid #d2d2d2; 226 | box-shadow:inset #fff 0 1px 0, inset rgba(0,0,0,0.03) 0 -1px 0; 227 | width:270px; 228 | } 229 | 230 | header .buttons li { 231 | width:89px; 232 | float:left; 233 | border-right:1px solid #d2d2d2; 234 | height:40px; 235 | } 236 | 237 | header ul.buttons a { 238 | line-height:1; 239 | font-size:11px; 240 | color:#999; 241 | display:block; 242 | text-align:center; 243 | padding-top:6px; 244 | height:40px; 245 | } 246 | 247 | strong { 248 | color:#222; 249 | font-weight:700; 250 | } 251 | 252 | header ul.buttons li + li { 253 | width:88px; 254 | border-left:1px solid #fff; 255 | } 256 | 257 | header ul.buttons li + li + li { 258 | border-right:none; 259 | width:89px; 260 | } 261 | 262 | header ul.buttons a strong { 263 | font-size:14px; 264 | display:block; 265 | color:#222; 266 | } 267 | 268 | section { 269 | width:500px; 270 | float:right; 271 | padding-bottom:5px; 272 | } 273 | 274 | small { 275 | font-size:11px; 276 | } 277 | 278 | hr { 279 | border:0; 280 | background:#e5e5e5; 281 | height:1px; 282 | margin:0 0 20px; 283 | } 284 | 285 | footer { 286 | width:270px; 287 | float:left; 288 | position:fixed; 289 | bottom:50px; 290 | margin-top: 4em; 291 | } 292 | 293 | 294 | 295 | form{ 296 | margin-bottom: 80px; 297 | } 298 | 299 | form p{ 300 | margin: 0 0 8px 0; 301 | } 302 | 303 | input[type="text"]{ 304 | width: 325px; 305 | padding: 7px; 306 | border: 1px solid #999; 307 | border-radius: 3px 3px 3px 3px; 308 | font-size: 12px; 309 | color: #333; 310 | } 311 | 312 | input[type="submit"], input[type="button"]{ 313 | margin-top: 10px; 314 | font-size: 2em; 315 | } 316 | 317 | label{ 318 | display: inline-block; 319 | } 320 | 321 | textarea { 322 | width: 340px; 323 | height: 150px; 324 | padding: 7px; 325 | border: 1px solid #999; 326 | border-radius: 3px 3px 3px 3px; 327 | font-size: 12px; 328 | color: #333; 329 | } 330 | 331 | xxxul { 332 | float: left; 333 | } 334 | 335 | .button { 336 | background: #33a0e8; 337 | background: -moz-linear-gradient(top, #33a0e8 0%, #2180ce 100%); 338 | background: -webkit-gradient(linear, left top, left bottom, color-stop(0%,#33a0e8), color-stop(100%,#2180ce)); 339 | background: -webkit-linear-gradient(top, #33a0e8 0%,#2180ce 100%); 340 | background: -o-linear-gradient(top, #33a0e8 0%,#2180ce 100%); 341 | background: -ms-linear-gradient(top, #33a0e8 0%,#2180ce 100%); 342 | background: linear-gradient(to bottom, #33a0e8 0%,#2180ce 100%); 343 | filter: progid:DXImageTransform.Microsoft.gradient( startColorstr='#33a0e8', endColorstr='#2180ce',GradientType=0 ); 344 | border-color: #2270AB #18639A #0F568B; 345 | border-style: solid; 346 | border-width: 1px; 347 | box-shadow: 0 1px 1px rgba(0, 0, 0, 0.3), 0 1px 0 #83C5F1 inset; 348 | color: #FFFFFF; 349 | border-radius: 3px 3px 3px 3px; 350 | cursor: pointer; 351 | font-size: 13px; 352 | font-weight: 600; 353 | overflow: visible; 354 | padding: 5px 16px; 355 | text-align: center; 356 | } 357 | 358 | .button:active, .button-down { 359 | background: #2180ce; 360 | background: -moz-linear-gradient(top, #2180ce 0%, #33a0e8 100%); 361 | background: -webkit-gradient(linear, left top, left bottom, color-stop(0%,#2180ce), color-stop(100%,#33a0e8)); 362 | background: -webkit-linear-gradient(top, #2180ce 0%,#33a0e8 100%); 363 | background: -o-linear-gradient(top, #2180ce 0%,#33a0e8 100%); 364 | background: -ms-linear-gradient(top, #2180ce 0%,#33a0e8 100%); 365 | background: linear-gradient(to bottom, #2180ce 0%,#33a0e8 100%); 366 | filter: progid:DXImageTransform.Microsoft.gradient( startColorstr='#2180ce', endColorstr='#33a0e8',GradientType=0 ); 367 | } 368 | 369 | .large-input{ 370 | padding: 7px; 371 | border: 1px solid #999; 372 | border-radius: 3px 3px 3px 3px; 373 | font-size: 14px; 374 | } 375 | 376 | .large-label{ 377 | display: block; 378 | width: 100%; 379 | margin-bottom: 16px; 380 | } 381 | 382 | input.large-button{ 383 | margin-left: 0; 384 | } 385 | 386 | #loading{ 387 | display: none; 388 | margin-left: 6px; 389 | vertical-align: top; 390 | } 391 | 392 | #demos{ 393 | margin-bottom: 60px; 394 | font-weight: bold; 395 | } 396 | 397 | .code-title{ 398 | margin-top: 2.2em; 399 | } 400 | 401 | .services{ 402 | margin-top: 2em; 403 | list-style: none; 404 | margin-bottom: 30px; 405 | width: 20em; 406 | } 407 | 408 | .services li{ 409 | height: 24px; 410 | margin-bottom: 2px; 411 | background-size: 16px 16px; 412 | font-size: 14px; 413 | } 414 | 415 | .services li span{ 416 | display: inline-block; 417 | margin-right: 10px; 418 | color: #666; 419 | } 420 | 421 | .services li a{ 422 | font-weight: bold; 423 | } 424 | 425 | ul.services{ 426 | float: none; 427 | } 428 | 429 | #support{ 430 | margin-bottom: 6em; 431 | } 432 | 433 | #support .services{ 434 | margin-bottom: 0; 435 | } 436 | 437 | #support-lists ul{ 438 | float: left; 439 | } 440 | 441 | #support-lists .services{ 442 | width: auto; 443 | } 444 | 445 | #support-lists .services li{ 446 | border: none; 447 | } 448 | 449 | 450 | .notes{ 451 | clear: both; 452 | margin-bottom: 60px; 453 | margin-left: 40px; 454 | } 455 | 456 | .error{ 457 | margin-bottom: 60px; 458 | } 459 | 460 | 461 | .paging{ 462 | list-style: none; 463 | padding: 0; 464 | margin: 0; 465 | } 466 | 467 | .paging li{ 468 | padding: 2px 8px; 469 | margin: 2px; 470 | background-color: white; 471 | border: solid 1px #39C; 472 | display: block; 473 | float: left; 474 | } 475 | 476 | .paging li.current-page{ 477 | border: solid 1px #ddd; 478 | color: #ddd; 479 | } 480 | 481 | 482 | .clearfix:after { 483 | visibility: hidden; 484 | display: block; 485 | font-size: 0; 486 | content: " "; 487 | clear: both; 488 | height: 0; 489 | } 490 | * html .clearfix { zoom: 1; } /* IE6 */ 491 | *:first-child+html .clearfix { zoom: 1; } /* IE7 */ 492 | 493 | 494 | 495 | 496 | 497 | 498 | div.swagger-ui-wrap h2, div.swagger-ui-wrap h3{ 499 | margin: 0; 500 | text-transform: none; 501 | } 502 | 503 | div.swagger-ui-wrap input[type="text"]{ 504 | padding: 2px; 505 | width: 220px; 506 | } 507 | 508 | div.swagger-ui-wrap form{ 509 | margin: 0; 510 | } 511 | 512 | div.swagger-ui-wrap ul{ 513 | margin: 0; 514 | } 515 | 516 | div.footer{ 517 | display: none; 518 | } 519 | 520 | #message-bar{ 521 | min-height: inherit; 522 | padding: 0; 523 | } 524 | 525 | 526 | 527 | @media print, screen and (max-width: 800px) { 528 | .wrapper { 529 | width: 100%; 530 | } 531 | } 532 | 533 | 534 | 535 | @media print, screen and (max-width: 720px) { 536 | body { 537 | word-wrap:break-word; 538 | } 539 | 540 | pre, code { 541 | word-wrap:normal; 542 | } 543 | } 544 | 545 | @media print, screen and (max-width: 480px) { 546 | body { 547 | padding:15px; 548 | } 549 | 550 | .menu li { 551 | float: none; 552 | } 553 | 554 | .menu li a { 555 | border: 0; 556 | padding-left: 0em; 557 | padding-top: 0.4em; 558 | } 559 | 560 | .menu .lastItem{ 561 | border: none; 562 | } 563 | 564 | .services { 565 | width: 15em; 566 | } 567 | 568 | input[type="text"]{ 569 | width: 200px; 570 | } 571 | 572 | 573 | } 574 | 575 | @media print { 576 | body { 577 | padding:0.4in; 578 | font-size:12pt; 579 | color:#444; 580 | } 581 | } -------------------------------------------------------------------------------- /public/images/dropbox.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 18 | 20 | 23 | 27 | 28 | 31 | 35 | 36 | 39 | 43 | 44 | 47 | 51 | 52 | 55 | 59 | 60 | 63 | 67 | 68 | 71 | 75 | 76 | 79 | 83 | 84 | 87 | 91 | 92 | 95 | 99 | 100 | 103 | 107 | 108 | 111 | 115 | 116 | 119 | 123 | 124 | 127 | 131 | 132 | 135 | 139 | 140 | 143 | 147 | 148 | 151 | 156 | 157 | 160 | 164 | 165 | 168 | 172 | 173 | 176 | 180 | 181 | 184 | 188 | 189 | 192 | 196 | 197 | 200 | 204 | 205 | 208 | 212 | 213 | 216 | 220 | 221 | 224 | 228 | 229 | 232 | 236 | 237 | 240 | 244 | 245 | 248 | 252 | 253 | 256 | 260 | 261 | 264 | 268 | 269 | 272 | 276 | 277 | 278 | 300 | 302 | 303 | 305 | image/svg+xml 306 | 308 | 309 | 310 | 311 | 312 | 317 | 320 | 323 | 326 | 331 | 332 | 335 | 340 | 341 | 344 | 349 | 350 | 353 | 358 | 359 | 362 | 367 | 368 | 369 | 370 | 371 | 372 | -------------------------------------------------------------------------------- /lib/routes.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const Joi = require('joi'); 4 | const handlers = require('../lib/handlers.js'); 5 | 6 | 7 | 8 | const sumModel = Joi.object({ 9 | id: Joi.string().required().example('x78P9c'), 10 | a: Joi.number().required().example(5), 11 | b: Joi.number().required().example(5), 12 | operator: Joi.string().required().description('either +, -, /, or *').example('+'), 13 | equals: Joi.number().required().example(10), 14 | created: Joi.string().required().isoDate().description('ISO date string').example('2015-12-01'), 15 | modified: Joi.string().isoDate().description('ISO date string').example('2015-12-01') 16 | }).label('Sum').description('json body for sum'); 17 | 18 | 19 | const listModel = Joi.object({ 20 | items: Joi.array().items(sumModel), 21 | count: Joi.number().required().example('1'), 22 | pageSize: Joi.number().required().example('10'), 23 | page: Joi.number().required().example('1'), 24 | pageCount: Joi.number().required().example('1') 25 | }).label('List'); 26 | 27 | 28 | const resultModel = Joi.object({ 29 | equals: Joi.number() 30 | }).label('Result'); 31 | 32 | 33 | const errorModel = Joi.object({ 34 | code: Joi.number(), 35 | msg: Joi.string() 36 | }).label('Error'); 37 | 38 | 39 | const sumHTTPStatus = { 40 | '200': { 41 | 'description': 'Success', 42 | 'schema': sumModel 43 | }, 44 | '400': { 45 | 'description': 'Bad Request', 46 | 'schema': errorModel 47 | }, 48 | '500': { 49 | 'description': 'Internal Server Error', 50 | 'schema': errorModel 51 | } 52 | }; 53 | 54 | 55 | const listHTTPStatus = { 56 | '200': { 57 | 'description': 'Success', 58 | 'schema': listModel 59 | }, 60 | '400': { 61 | 'description': 'Bad Request' 62 | }, 63 | '404': { 64 | 'description': 'Sum not found' 65 | }, 66 | '500': { 67 | 'description': 'Internal Server Error' 68 | } 69 | }; 70 | 71 | 72 | const fileHTTPStatus = { 73 | '200': { 74 | 'description': 'Success', 75 | 'schema': sumModel 76 | }, 77 | '400': { 78 | 'description': 'Bad Request' 79 | }, 80 | '404': { 81 | 'description': 'Unsupported Media Type' 82 | }, 83 | '500': { 84 | 'description': 'Internal Server Error' 85 | } 86 | }; 87 | 88 | 89 | const resultHTTPStatus = { 90 | '200': { 91 | 'description': 'Success', 92 | 'schema': resultModel 93 | }, 94 | '400': { 95 | 'description': 'Bad Request' 96 | }, 97 | '404': { 98 | 'description': 'Sum not found' 99 | }, 100 | '500': { 101 | 'description': 'Internal Server Error' 102 | } 103 | }; 104 | 105 | module.exports = [{ 106 | method: 'GET', 107 | path: '/', 108 | config: { 109 | auth: { 110 | mode: 'optional' 111 | }, 112 | handler: handlers.index 113 | } 114 | },{ 115 | method: 'GET', 116 | path: '/login', 117 | config: { 118 | auth: 'github-oauth', 119 | handler: handlers.login 120 | } 121 | },{ 122 | method: 'GET', 123 | path: '/account', 124 | config: { 125 | handler: handlers.account 126 | } 127 | },{ 128 | method: 'GET', 129 | path: '/logout', 130 | config: { 131 | auth: false, 132 | handler: handlers.logout 133 | } 134 | },{ 135 | method: 'GET', 136 | path: '/license', 137 | config: { 138 | auth: false, 139 | handler: handlers.license 140 | } 141 | },{ 142 | method: 'PUT', 143 | path: '/sum/add/{a}/{b}', 144 | config: { 145 | auth: false, 146 | handler: handlers.add, 147 | description: 'Add', 148 | tags: ['api', 'reduced'], 149 | notes: ['Adds together two numbers and return the result. As an option you can have the result return as a binary number.'], 150 | plugins: { 151 | 'hapi-swagger': { 152 | responses: resultHTTPStatus 153 | } 154 | }, 155 | validate: { 156 | params: { 157 | a: Joi.number() 158 | .required() 159 | .description('the first number'), 160 | 161 | b: Joi.number() 162 | .required() 163 | .description('the second number') 164 | }, 165 | headers: Joi.object({ 166 | 'x-format': Joi.string() 167 | .valid('decimal', 'binary') 168 | .default('decimal') 169 | .description('return result as decimal or binary') 170 | }).unknown() 171 | } 172 | } 173 | }, { 174 | method: 'PUT', 175 | path: '/sum/subtract/{a}/{b}', 176 | config: { 177 | auth: false, 178 | handler: handlers.subtract, 179 | description: 'Subtract', 180 | notes: ['Subtracts the second number from the first and return the result'], 181 | tags: ['api'], 182 | plugins: { 183 | 'hapi-swagger': { 184 | responses: resultHTTPStatus 185 | } 186 | }, 187 | validate: { 188 | params: { 189 | a: Joi.number() 190 | .required() 191 | .description('the first number'), 192 | 193 | b: Joi.number() 194 | .required() 195 | .description('the second number') 196 | } 197 | } 198 | } 199 | }, { 200 | method: 'PUT', 201 | path: '/sum/divide/{a}/{b}', 202 | config: { 203 | auth: false, 204 | handler: handlers.divide, 205 | description: 'Divide', 206 | notes: ['Divides the first number by the second and return the result'], 207 | tags: ['api'], 208 | plugins: { 209 | 'hapi-swagger': { 210 | responses: resultHTTPStatus 211 | } 212 | }, 213 | validate: { 214 | params: { 215 | a: Joi.number() 216 | .required() 217 | .description('the first number - can NOT be 0'), 218 | 219 | b: Joi.number() 220 | .required() 221 | .description('the second number - can NOT be 0') 222 | } 223 | } 224 | } 225 | }, { 226 | method: 'PUT', 227 | path: '/sum/multiple/{a}/{b}', 228 | config: { 229 | auth: false, 230 | handler: handlers.multiple, 231 | description: 'Multiple', 232 | notes: ['Multiples the two numbers together and return the result'], 233 | plugins: { 234 | 'hapi-swagger': { 235 | responses: resultHTTPStatus 236 | } 237 | }, 238 | tags: ['api'], 239 | validate: { 240 | params: { 241 | a: Joi.number() 242 | .required() 243 | .description('the first number'), 244 | 245 | b: Joi.number() 246 | .required() 247 | .description('the second number') 248 | } 249 | } 250 | } 251 | }, { 252 | method: 'GET', 253 | path: '/store/', 254 | config: { 255 | auth: 'jwt', 256 | handler: handlers.storeList, 257 | description: 'List sums', 258 | notes: ['List the sums in the data store'], 259 | plugins: { 260 | 'hapi-swagger': { 261 | responses: listHTTPStatus 262 | } 263 | }, 264 | tags: ['api', 'reduced', 'one'], 265 | validate: { 266 | query: { 267 | page: Joi.number() 268 | .description('the page number'), 269 | 270 | pagesize: Joi.number() 271 | .description('the number of items to a page') 272 | } 273 | } 274 | } 275 | }, { 276 | method: 'GET', 277 | path: '/store/{id}', 278 | config: { 279 | auth: 'jwt', 280 | handler: handlers.storeItem, 281 | description: 'Get sum', 282 | notes: ['Get a sum from the store'], 283 | plugins: { 284 | 'hapi-swagger': { 285 | responses: sumHTTPStatus 286 | } 287 | }, 288 | tags: ['api', 'reduced', 'two'], 289 | validate: { 290 | params: { 291 | id: Joi.string() 292 | .required() 293 | .description('the id of the sum in the store') 294 | } 295 | } 296 | } 297 | }, { 298 | method: 'POST', 299 | path: '/store/', 300 | config: { 301 | auth: 'jwt', 302 | handler: handlers.storeAdd, 303 | description: 'Add sum', 304 | notes: ['Adds a sum to the data store'], 305 | plugins: { 306 | 'hapi-swagger': { 307 | responses: sumHTTPStatus, 308 | payloadType: 'form', 309 | nickname: 'storeit' 310 | } 311 | }, 312 | tags: ['api', 'reduced', 'three'], 313 | validate: { 314 | payload: { 315 | a: Joi.number() 316 | .required() 317 | .description('the first number'), 318 | 319 | b: Joi.number() 320 | .required() 321 | .description('the second number'), 322 | 323 | operator: Joi.string() 324 | .required() 325 | .default('+') 326 | .valid(['+','-','/','*']) 327 | .description('the opertator i.e. + - / or *'), 328 | 329 | equals: Joi.number() 330 | .required() 331 | .description('the result of the sum') 332 | } 333 | } 334 | } 335 | }, { 336 | method: 'PUT', 337 | path: '/store/{id}', 338 | config: { 339 | auth: 'jwt', 340 | handler: handlers.storeUpdate, 341 | description: 'Update sum', 342 | notes: ['Update a sum in our data store'], 343 | plugins: { 344 | 'hapi-swagger': { 345 | responses: sumHTTPStatus, 346 | payloadType: 'form' 347 | } 348 | }, 349 | tags: ['api'], 350 | validate: { 351 | params: { 352 | id: Joi.string() 353 | .required() 354 | .description('the id of the sum in the store') 355 | }, 356 | payload: { 357 | a: Joi.number() 358 | .required() 359 | .description('the first number'), 360 | 361 | b: Joi.number() 362 | .required() 363 | .description('the second number'), 364 | 365 | operator: Joi.string() 366 | .required() 367 | .default('+') 368 | .valid(['+','-','/','*']) 369 | .description('the opertator i.e. + - / or *'), 370 | 371 | equals: Joi.number() 372 | .required() 373 | .description('the result of the sum') 374 | } 375 | } 376 | } 377 | }, { 378 | method: 'DELETE', 379 | path: '/store/{id}', 380 | config: { 381 | auth: 'jwt', 382 | handler: handlers.storeRemove, 383 | description: 'Delete sums', 384 | notes: ['Delete a sums from the data store'], 385 | plugins: { 386 | 'hapi-swagger': { 387 | responses: sumHTTPStatus 388 | } 389 | }, 390 | tags: ['api'], 391 | validate: { 392 | params: { 393 | id: Joi.string() 394 | .required() 395 | .description('the id of the sum in the store') 396 | } 397 | } 398 | } 399 | }, { 400 | method: 'POST', 401 | path: '/store/payload/', 402 | config: { 403 | auth: 'jwt', 404 | handler: handlers.storeAdd, 405 | description: 'Add sum, with JSON object', 406 | notes: ['Adds a sum to the data store, using JSON object in payload'], 407 | plugins: { 408 | 'hapi-swagger': { 409 | responses: sumHTTPStatus 410 | } 411 | }, 412 | tags: ['api', 'reduced', 'three'], 413 | validate: { 414 | payload: { 415 | a: Joi.number() 416 | .required() 417 | .description('the first number'), 418 | 419 | b: Joi.number() 420 | .required() 421 | .description('the second number'), 422 | 423 | operator: Joi.string() 424 | .required() 425 | .default('+') 426 | .valid(['+','-','/','*']) 427 | .description('the opertator i.e. + - / or *'), 428 | 429 | equals: Joi.number() 430 | .required() 431 | .description('the result of the sum') 432 | } 433 | } 434 | } 435 | }, { 436 | method: 'POST', 437 | path: '/store/file/', 438 | config: { 439 | auth: 'jwt', 440 | handler: handlers.storeAddFile, 441 | description: 'Add sum, with JSON file', 442 | notes: ['Adds a sum to the data store, using JSON object in a uploaded file'], 443 | plugins: { 444 | 'hapi-swagger': { 445 | responses: fileHTTPStatus, 446 | payloadType: 'form' 447 | } 448 | }, 449 | tags: ['api', 'reduced', 'three'], 450 | validate: { 451 | payload: { 452 | file: Joi.any() 453 | .meta({ swaggerType: 'file' }) 454 | .required() 455 | .description('json file with object containing: a, b, operator and equals') 456 | } 457 | }, 458 | payload: { 459 | maxBytes: 1048576, 460 | parse: true, 461 | output: 'stream' 462 | } 463 | } 464 | }, { 465 | method: 'GET', 466 | path: '/{path*}', 467 | config: { 468 | auth: false, 469 | handler: { 470 | directory: { 471 | path: './public', 472 | listing: false, 473 | index: true 474 | } 475 | } 476 | } 477 | }]; 478 | 479 | 480 | --------------------------------------------------------------------------------