├── .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 |
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 |
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 |
183 |
184 |
API Access
185 | {{> account home=true}}
186 |
187 |
188 |
189 |
190 |
194 |
195 |
196 | {{#if hapiSwagger.info.license}}
197 |
203 | {{/if}}
204 |
205 | {{> footer}}
206 |
207 |
208 |
209 |
210 |