├── .env.example
├── .github
├── dependabot.yml
└── workflows
│ └── node.js.yml
├── .gitignore
├── .jscsrc
├── LICENSE
├── Procfile
├── README.md
├── config.js
├── controllers
├── availableNumbers.js
├── dashboard.js
├── leadSources.js
├── leads.js
└── router.js
├── images
├── app-configuration.png
├── main-dashboard.png
├── phone-search.png
├── purchase-number.png
├── setup-lead.png
└── webhook.png
├── index.js
├── models
├── Lead.js
└── LeadSource.js
├── package.json
├── public
├── 404.html
├── 500.html
├── css
│ └── main.css
└── js
│ └── pieCharts.js
├── test
├── dashboardTest.js
├── leadSourcesTest.js
├── leadsTest.js
└── testHelper.js
├── util
└── twimlApp.js
├── views
├── availableNumbers.jade
├── dashboard.jade
├── editLeadSource.jade
└── layout.jade
└── webapp.js
/.env.example:
--------------------------------------------------------------------------------
1 | # Find this at https://www.twilio.com/console
2 | TWILIO_ACCOUNT_SID=ACXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX
3 |
4 | # SID of your TwiML Application
5 | # Go to the console to view and/or create a new twiml app:
6 | # https://www.twilio.com/console/voice/twiml/apps
7 | # OR you can create a new app from the command line:
8 | # twilio api:core:applications:create --friendly-name=voice-client-javascript
9 | TWILIO_APP_SID=APXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX
10 |
11 | # Your REST API Key information
12 | # View keys or create a new key from the console:
13 | # https://www.twilio.com/console/project/api-keys
14 | # NOTE: Make sure to copy the secret, it will only be displayed once
15 | TWILIO_API_KEY=SKXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX
16 | TWILIO_API_SECRET=XXXXXXXXXXXXXXXXX
17 |
18 | # Your mongodb connection string i.e. mongodb://localhost:27017/call-tracking
19 | # If you leave this variable commented out, the application will create a new
20 | # local mongodb database called "call-tracking"
21 | # MONGO_URL=
22 |
23 | NODE_ENV=production
24 |
--------------------------------------------------------------------------------
/.github/dependabot.yml:
--------------------------------------------------------------------------------
1 | version: 2
2 | updates:
3 | - package-ecosystem: npm
4 | directory: "/"
5 | schedule:
6 | interval: daily
7 | open-pull-requests-limit: 10
8 | ignore:
9 | - dependency-name: express-validator
10 | versions:
11 | - 6.10.0
12 | - 6.9.2
13 | - dependency-name: mongoose
14 | versions:
15 | - 5.11.14
16 | - 5.11.15
17 | - 5.11.16
18 | - 5.11.17
19 | - 5.11.18
20 | - 5.11.19
21 | - 5.12.0
22 | - 5.12.1
23 | - 5.12.2
24 | - 5.12.3
25 | - 5.12.4
26 | - 5.12.5
27 | - dependency-name: twilio
28 | versions:
29 | - 3.51.0
30 | - 3.56.0
31 | - 3.57.0
32 | - 3.58.0
33 | - 3.59.0
34 | - 3.60.0
35 | - dependency-name: chai
36 | versions:
37 | - 4.2.0
38 | - 4.3.0
39 | - 4.3.1
40 | - 4.3.3
41 | - dependency-name: sinon
42 | versions:
43 | - 10.0.0
44 | - 9.2.4
45 | - dependency-name: http-auth
46 | versions:
47 | - 4.1.2
48 | - 4.1.3
49 | - 4.1.4
50 | - dependency-name: mocha
51 | versions:
52 | - 8.2.1
53 | - 8.3.0
54 | - 8.3.1
55 | - dependency-name: nock
56 | versions:
57 | - 13.0.10
58 | - 13.0.6
59 | - 13.0.7
60 | - 13.0.8
61 | - 13.0.9
62 |
--------------------------------------------------------------------------------
/.github/workflows/node.js.yml:
--------------------------------------------------------------------------------
1 | # This workflow will do a clean install of node dependencies, cache/restore them, build the source code and run tests across different versions of node
2 | # For more information see: https://help.github.com/actions/language-and-framework-guides/using-nodejs-with-github-actions
3 |
4 | name: Node.js CI
5 |
6 | on:
7 | push:
8 | branches: [ master ]
9 | paths-ignore:
10 | - '**.md'
11 | pull_request:
12 | branches: [ master ]
13 | paths-ignore:
14 | - '**.md'
15 |
16 | jobs:
17 | build:
18 | runs-on: ubuntu-latest
19 |
20 | strategy:
21 | matrix:
22 | node-version: [14]
23 |
24 | steps:
25 | - uses: actions/checkout@v2
26 | - name: Use Node.js ${{ matrix.node-version }}
27 | uses: actions/setup-node@v2
28 | with:
29 | node-version: ${{ matrix.node-version }}
30 |
31 | - name: Start MongoDB
32 | uses: supercharge/mongodb-github-action@1.6.0
33 | with:
34 | mongodb-version: 5
35 | - name: Create env file
36 | run: cp .env.example .env
37 |
38 | - name: Install Dependencies
39 | run: npm install
40 | - run: npm run build --if-present
41 | - run: npm test
42 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 |
2 | .DS_Store
3 |
4 | # Logs
5 | logs
6 | *.log
7 |
8 | # Runtime data
9 | pids
10 | *.pid
11 | *.seed
12 |
13 | # Directory for instrumented libs generated by jscoverage/JSCover
14 | lib-cov
15 |
16 | # Coverage directory used by tools like istanbul
17 | coverage
18 |
19 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files)
20 | .grunt
21 |
22 | # node-waf configuration
23 | .lock-wscript
24 |
25 | # Compiled binary addons (http://nodejs.org/api/addons.html)
26 | build/Release
27 |
28 | # Dependency directory
29 | # https://www.npmjs.org/doc/misc/npm-faq.html#should-i-check-my-node_modules-folder-into-git
30 | node_modules
31 |
32 | # Development/test local configuration
33 | .env
34 |
--------------------------------------------------------------------------------
/.jscsrc:
--------------------------------------------------------------------------------
1 | {
2 | "requireCurlyBraces": [
3 | "if",
4 | "else",
5 | "for",
6 | "while",
7 | "do",
8 | "try",
9 | "catch"
10 | ],
11 | "requireOperatorBeforeLineBreak": true,
12 | "requireCamelCaseOrUpperCaseIdentifiers": true,
13 | "maximumLineLength": {
14 | "value": 80,
15 | "allExcept": ["comments", "regex"]
16 | },
17 | "validateIndentation": 2,
18 | "validateQuoteMarks": "'",
19 |
20 | "disallowMultipleLineStrings": true,
21 | "disallowMixedSpacesAndTabs": true,
22 | "disallowTrailingWhitespace": true,
23 | "disallowSpaceAfterPrefixUnaryOperators": true,
24 | "disallowMultipleVarDecl": true,
25 | "disallowKeywordsOnNewLine": ["else"],
26 |
27 | "requireSpaceAfterKeywords": [
28 | "if",
29 | "else",
30 | "for",
31 | "while",
32 | "do",
33 | "switch",
34 | "return",
35 | "try",
36 | "catch"
37 | ],
38 | "requireSpaceBeforeBinaryOperators": [
39 | "=", "+=", "-=", "*=", "/=", "%=", "<<=", ">>=", ">>>=",
40 | "&=", "|=", "^=", "+=",
41 |
42 | "+", "-", "*", "/", "%", "<<", ">>", ">>>", "&",
43 | "|", "^", "&&", "||", "===", "==", ">=",
44 | "<=", "<", ">", "!=", "!=="
45 | ],
46 | "requireSpaceAfterBinaryOperators": true,
47 | "requireSpacesInConditionalExpression": true,
48 | "requireSpaceBeforeBlockStatements": true,
49 | "requireSpacesInForStatement": true,
50 | "requireLineFeedAtFileEnd": true,
51 | "requireSpacesInFunctionExpression": {
52 | "beforeOpeningCurlyBrace": true
53 | },
54 | "disallowSpacesInAnonymousFunctionExpression": {
55 | "beforeOpeningRoundBrace": true
56 | },
57 | "disallowSpacesInsideObjectBrackets": "all",
58 | "disallowSpacesInsideArrayBrackets": "all",
59 | "disallowSpacesInsideParentheses": true,
60 |
61 | "disallowMultipleLineBreaks": true,
62 | "disallowNewlineBeforeBlockStatements": true,
63 | "disallowKeywords": ["with"],
64 | "disallowSpacesInFunctionExpression": {
65 | "beforeOpeningRoundBrace": true
66 | },
67 | "disallowSpacesInFunctionDeclaration": {
68 | "beforeOpeningRoundBrace": true
69 | },
70 | "disallowSpacesInCallExpression": true,
71 | "disallowSpaceAfterObjectKeys": true,
72 | "requireSpaceBeforeObjectValues": true,
73 | "requireCapitalizedConstructors": true,
74 | "requireDotNotation": true,
75 | "requireSemicolons": true,
76 | "validateParameterSeparator": ", ",
77 |
78 | "jsDoc": {
79 | "checkAnnotations": "closurecompiler",
80 | "checkParamNames": true,
81 | "requireParamTypes": true,
82 | "checkRedundantParams": true,
83 | "checkReturnTypes": true,
84 | "checkRedundantReturns": true,
85 | "requireReturnTypes": true,
86 | "checkTypes": true,
87 | "checkRedundantAccess": true,
88 | "requireNewlineAfterDescription": true
89 | }
90 | }
91 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/TwilioDevEd/call-tracking-node/e2844e92c219c612ca558d08069710bbe8aae2b8/LICENSE
--------------------------------------------------------------------------------
/Procfile:
--------------------------------------------------------------------------------
1 | web: node index.js
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | # Call tracking
6 |
7 | [](https://github.com/TwilioDevEd/call-tracking-node/actions/workflows/node.js.yml)
8 |
9 | Call Tracking helps you measure the effectiveness of different marketing campaigns. By assigning a unique phone number to different advertisements, you can track which ones have the best call rates and get some data about the callers themselves. For a step-by-step tutorial see [twilio docs](https://www.twilio.com/docs/tutorials/walkthrough/call-tracking/node/express) to help with setting this up.
10 |
11 | ### Create a TwiML App
12 |
13 | This project is configured to use a **TwiML App**, which allows us to easily set the voice URLs for all Twilio phone numbers we purchase in this app.
14 |
15 | [Create a new TwiML app](https://www.twilio.com/console/voice/twiml/apps) and use its `Sid` as the `TWILIO_APP_SID` environment variable wherever you run this app.
16 |
17 | You'll configure the exact URL to use in your TwiML app in the ["Try it out"](#try-it-out) section of this application.
18 |
19 | ## Local development
20 |
21 | ### Prerequisites
22 |
23 | To run this project locally, you'll need to install:
24 | - [Node.js](http://nodejs.org/) which should also install [npm](https://www.npmjs.com/).
25 | - [MongoDB](https://docs.mongodb.com/manual/administration/install-community/)
26 | - [ngrok](https://ngrok.com/download)
27 |
28 | ### Setup
29 |
30 | 1. First clone this repository and `cd` into its directory:
31 | ```bash
32 | git clone https://github.com/TwilioDevEd/call-tracking-node.git
33 |
34 | cd call-tracking-node
35 | ```
36 |
37 | 1. Install dependencies:
38 | ```bash
39 | npm install
40 | ```
41 |
42 | 1. Copy the sample configuration file and edit it to match your configuration.
43 | ```bash
44 | cp .env.example .env
45 | ```
46 |
47 | The `.env` file lists where you can find or generate the values for each required variable.
48 |
49 | Run `source .env` to export the environment variables.
50 |
51 | 1. Start the MongoDB server.
52 |
53 | This app requires MongoDB to be running. See how to start the MongoDB service on [Windows](https://docs.mongodb.com/manual/tutorial/install-mongodb-on-windows/#start-mongodb-community-edition-as-a-windows-service), [MacOS](https://docs.mongodb.com/manual/tutorial/install-mongodb-on-os-x/#run-mongodb-community-edition), or [Linux](https://docs.mongodb.com/manual/administration/install-on-linux/) (choose your Linux distribution and then see "Run MongoDB Community Edition" in the installation instructions).
54 |
55 | 1. Run the application.
56 | ```bash
57 | npm start
58 | ```
59 |
60 | Alternatively you might also consider using [nodemon](https://github.com/remy/nodemon) for this. It works just like
61 | the node command, but automatically restarts your application when you change any source code files.
62 |
63 | ```bash
64 | npm install -g nodemon
65 | nodemon ./bin/www
66 | ```
67 |
68 | You should now be able to visit `http://localhost:3000` on your local web browser and see a blank dashboard.
69 | The app is almost ready to go!
70 |
71 | 1. Start ngrok
72 |
73 | To actually forward incoming calls, your development server will need to be publicly accessible, so that Twilio can communicate with it. [We recommend using ngrok to do this](https://www.twilio.com/blog/2015/09/6-awesome-reasons-to-use-ngrok-when-testing-webhooks.html). Install [ngrok](http://ngrok.com) and then run it, exposing port 3000 (the port that your local server is running on):
74 |
75 | ```bash
76 | ngrok http 3000
77 | ```
78 |
79 | You will use the ngrok tunnel URL provided in the ["Try it out"](#try-it-out) step below.
80 |
81 | ## Run the tests
82 |
83 | You can run the tests locally by typing
84 |
85 | ```bash
86 | npm test
87 | ```
88 |
89 | ## Try it out
90 |
91 | In your Twilio app configuration you'll need to set
92 | `http://.ngrok.io/lead` as the callback URL. Open
93 | the application and then click the "App configuration" button.
94 |
95 | 
96 |
97 | The button will take you to your TwiML call tracking
98 | application. Under "Voice" you will find a "Request URL" input
99 | box. There you should put the URL to the application's lead resource
100 | (e.g `http://.ngrok.io/lead`).
101 |
102 | 
103 |
104 | You can now purchase new numbers from Twilio, associate them with a lead source,
105 | and set up call forwarding from the dashboard.
106 |
107 | To add a new number press the "Search" button on the main dashboard. You can optionally
108 | select an area code to find a number in that particular area.
109 |
110 | 
111 |
112 | After you click "Search", you will be shown a list of available Twilio numbers that you
113 | can purchase. To select and purchase a number, click the "Purchase" button next
114 | to any of the listed available phone numbers.
115 |
116 | 
117 |
118 | You will then be redirected to a form where you can label the Lead Source and
119 | set up call forwarding. Now, when someone calls the number you have just purchased,
120 | it will be forwarded to the number you configure under "Forwarding number".
121 |
122 | 
123 |
124 | Now, when someone calls the number you purchased and labeled, the call will display
125 | in your dashboard as having been generated from that specific lead.
126 |
127 | 
128 |
129 | ## Meta
130 |
131 | * No warranty expressed or implied. Software is as is. Diggity.
132 | * [MIT License](http://www.opensource.org/licenses/mit-license.html)
133 | * Lovingly crafted by Twilio Developer Education.
134 |
--------------------------------------------------------------------------------
/config.js:
--------------------------------------------------------------------------------
1 | // Do not allow the application to run if env vars not set in .env file
2 | require('dotenv-safe').load();
3 |
4 | var env = process.env;
5 |
6 | module.exports = {
7 | // HTTP Port to run our web application
8 | port: env.port || 3000,
9 | // A random string that will help generate secure one-time passwords and
10 | // HTTP sessions
11 | secret: env.APP_SECRET || 'keyboard cat',
12 | accountSid: env.TWILIO_ACCOUNT_SID,
13 | apiKey: env.TWILIO_API_KEY,
14 | apiSecret: env.TWILIO_API_SECRET,
15 | appSid: env.TWILIO_APP_SID,
16 | mongoUrl: env.MONGO_URL
17 | };
18 |
--------------------------------------------------------------------------------
/controllers/availableNumbers.js:
--------------------------------------------------------------------------------
1 | var twilio = require('twilio');
2 | var config = require('../config');
3 |
4 | var client = twilio(config.apiKey, config.apiSecret, { accountSid: config.accountSid });
5 |
6 | exports.index = function(request, response) {
7 |
8 | var areaCode = request.query.areaCode;
9 |
10 | client.availablePhoneNumbers('US').local.list({
11 | areaCode: areaCode
12 | }).then(function(availableNumbers) {
13 | response.render('availableNumbers', {
14 | availableNumbers: availableNumbers
15 | });
16 | }).catch(function(failureToFetchNumbers) {
17 | console.log('Failed to fetch numbers from API');
18 | console.log('Error was:');
19 | console.log(failureToFetchNumbers);
20 | response.status(500).send('Could not contact Twilio API');
21 | });
22 | };
23 |
--------------------------------------------------------------------------------
/controllers/dashboard.js:
--------------------------------------------------------------------------------
1 | var LeadSource = require('../models/LeadSource');
2 | var config = require('../config');
3 |
4 | exports.show = function(request, response) {
5 | LeadSource.find().then(function(leadSources) {
6 | return response.render('dashboard', {
7 | leadSources: leadSources,
8 | appSid: config.appSid
9 | });
10 | });
11 | };
12 |
--------------------------------------------------------------------------------
/controllers/leadSources.js:
--------------------------------------------------------------------------------
1 | var twilio = require('twilio');
2 | var config = require('../config');
3 | var LeadSource = require('../models/LeadSource');
4 |
5 | var client = twilio(config.apiKey, config.apiSecret, { accountSid: config.accountSid });
6 |
7 | exports.create = function(request, response) {
8 | var phoneNumberToPurchase = request.body.phoneNumber;
9 |
10 | client.incomingPhoneNumbers.create({
11 | phoneNumber: phoneNumberToPurchase,
12 | voiceCallerIdLookup: 'true',
13 | voiceApplicationSid: config.appSid
14 | }).then(function(purchasedNumber) {
15 | var leadSource = new LeadSource({number: purchasedNumber.phoneNumber});
16 | return leadSource.save();
17 | }).then(function(savedLeadSource) {
18 | console.log('Saving lead source');
19 | response.redirect(303, '/lead-source/' + savedLeadSource._id + '/edit');
20 | }).catch(function(numberPurchaseFailure) {
21 | console.log('Could not purchase a number for lead source:');
22 | console.log(numberPurchaseFailure);
23 | response.status(500).send('Could not contact Twilio API');
24 | });
25 | };
26 |
27 | exports.edit = function(request, response) {
28 | var leadSourceId = request.params.id;
29 | LeadSource.findOne({_id: leadSourceId}).then(function(foundLeadSource) {
30 | return response.render('editLeadSource', {
31 | leadSourceId: foundLeadSource._id,
32 | leadSourcePhoneNumber: foundLeadSource.number,
33 | leadSourceForwardingNumber: foundLeadSource.forwardingNumber,
34 | leadSourceDescription: foundLeadSource.description,
35 | messages: request.flash('error')
36 | });
37 | }).catch(function() {
38 | return response.status(404).send('No such lead source');
39 | });
40 | };
41 |
42 | exports.update = function(request, response) {
43 | var leadSourceId = request.params.id;
44 |
45 | request.checkBody('description', 'Description cannot be empty').notEmpty();
46 | request.checkBody('forwardingNumber', 'Forwarding number cannot be empty')
47 | .notEmpty();
48 |
49 | if (request.validationErrors()) {
50 | request.flash('error', request.validationErrors());
51 | return response.redirect(303, '/lead-source/' + leadSourceId + '/edit');
52 | }
53 |
54 | LeadSource.findOne({_id: leadSourceId}).then(function(foundLeadSource) {
55 | foundLeadSource.description = request.body.description;
56 | foundLeadSource.forwardingNumber = request.body.forwardingNumber;
57 |
58 | return foundLeadSource.save();
59 | }).then(function(savedLeadSource) {
60 | return response.redirect(303, '/dashboard');
61 | }).catch(function(error) {
62 | return response.status(500).send('Could not save the lead source');
63 | });
64 | };
65 |
--------------------------------------------------------------------------------
/controllers/leads.js:
--------------------------------------------------------------------------------
1 | var VoiceResponse = require('twilio').twiml.VoiceResponse;
2 | var _ = require('underscore');
3 |
4 | var LeadSource = require('../models/LeadSource');
5 | var Lead = require('../models/Lead');
6 | var config = require('../config');
7 |
8 | exports.create = function(request, response) {
9 | var leadSourceNumber = request.body.To;
10 |
11 | LeadSource.findOne({
12 | number: leadSourceNumber
13 | }).then(function(foundLeadSource) {
14 | var twiml = new VoiceResponse();
15 | twiml.dial(foundLeadSource.forwardingNumber);
16 |
17 | var newLead = new Lead({
18 | callerNumber: request.body.From,
19 | callSid: request.body.CallSid,
20 | leadSource: foundLeadSource._id,
21 | city: request.body.FromCity,
22 | state: request.body.FromState,
23 | callerName: request.body.CallerName
24 | });
25 | return newLead.save()
26 | .then(function() {
27 | response.send(twiml.toString());
28 | });
29 | }).catch(function(err) {
30 | console.log('Failed to forward call:');
31 | console.log(err);
32 | });
33 | };
34 |
35 | exports.leadsByLeadSource = function(request, response) {
36 | Lead.find()
37 | .populate('leadSource')
38 | .then(function(existingLeads) {
39 | var statsByLeadSource = _.countBy(existingLeads, function(lead) {
40 | return lead.leadSource.description;
41 | });
42 |
43 | response.send(statsByLeadSource);
44 | });
45 | };
46 |
47 | exports.leadsByCity = function(request, response) {
48 | Lead.find().then(function(existingLeads) {
49 | var statsByCity = _.countBy(existingLeads, 'city');
50 | response.send(statsByCity);
51 | });
52 | };
53 |
--------------------------------------------------------------------------------
/controllers/router.js:
--------------------------------------------------------------------------------
1 | var availableNumbers = require('./availableNumbers');
2 | var leadSources = require('./leadSources');
3 | var leads = require('./leads');
4 | var dashboard = require('./dashboard');
5 |
6 | // Map routes to controller functions
7 | exports.webRoutes = function(router) {
8 | router.get('/', function(req, resp) {
9 | return resp.redirect(302, '/dashboard');
10 | });
11 | router.get('/available-numbers', availableNumbers.index);
12 | router.post('/lead-source', leadSources.create);
13 | router.get('/lead-source/:id/edit', leadSources.edit);
14 | router.post('/lead-source/:id/update', leadSources.update);
15 | router.get('/dashboard', dashboard.show);
16 | router.get('/lead/summary-by-lead-source', leads.leadsByLeadSource);
17 | router.get('/lead/summary-by-city', leads.leadsByCity);
18 | };
19 |
20 | exports.webhookRoutes = function(router) {
21 | router.post('/lead', leads.create);
22 | };
23 |
--------------------------------------------------------------------------------
/images/app-configuration.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/TwilioDevEd/call-tracking-node/e2844e92c219c612ca558d08069710bbe8aae2b8/images/app-configuration.png
--------------------------------------------------------------------------------
/images/main-dashboard.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/TwilioDevEd/call-tracking-node/e2844e92c219c612ca558d08069710bbe8aae2b8/images/main-dashboard.png
--------------------------------------------------------------------------------
/images/phone-search.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/TwilioDevEd/call-tracking-node/e2844e92c219c612ca558d08069710bbe8aae2b8/images/phone-search.png
--------------------------------------------------------------------------------
/images/purchase-number.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/TwilioDevEd/call-tracking-node/e2844e92c219c612ca558d08069710bbe8aae2b8/images/purchase-number.png
--------------------------------------------------------------------------------
/images/setup-lead.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/TwilioDevEd/call-tracking-node/e2844e92c219c612ca558d08069710bbe8aae2b8/images/setup-lead.png
--------------------------------------------------------------------------------
/images/webhook.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/TwilioDevEd/call-tracking-node/e2844e92c219c612ca558d08069710bbe8aae2b8/images/webhook.png
--------------------------------------------------------------------------------
/index.js:
--------------------------------------------------------------------------------
1 | var http = require('http');
2 | var mongoose = require('mongoose');
3 | var config = require('./config');
4 |
5 | var connectionString = config.mongoUrl || "mongodb://localhost/call-tracking";
6 |
7 | // Initialize database connection - throws if database connection can't be
8 | // established
9 | mongoose.connect(connectionString, { useMongoClient: true });
10 | mongoose.Promise = Promise;
11 |
12 | // Create Express web app
13 | var app = require('./webapp');
14 |
15 | // Create an HTTP server and listen on the configured port
16 | var server = http.createServer(app);
17 | server.listen(config.port, function() {
18 | console.log('Express server listening on *:' + config.port);
19 | });
20 |
--------------------------------------------------------------------------------
/models/Lead.js:
--------------------------------------------------------------------------------
1 | var mongoose = require('mongoose');
2 |
3 | var LeadSchema = new mongoose.Schema({
4 | callerNumber: {
5 | type: String,
6 | required: true
7 | },
8 | callSid: {
9 | type: String,
10 | required: true
11 | },
12 | leadSource: {
13 | type: mongoose.Schema.Types.ObjectId,
14 | ref: 'LeadSource'
15 | },
16 | city: {
17 | type: String,
18 | required: false
19 | },
20 | state: {
21 | type: String,
22 | required: false
23 | },
24 | callerName: {
25 | type: String,
26 | required: false
27 | }
28 | });
29 |
30 | delete mongoose.models.Lead
31 |
32 | // Create a Mongoose model from our schema
33 | var Lead = mongoose.model('Lead', LeadSchema);
34 |
35 | // export model as our module interface
36 | module.exports = Lead;
37 |
--------------------------------------------------------------------------------
/models/LeadSource.js:
--------------------------------------------------------------------------------
1 | var mongoose = require('mongoose');
2 |
3 | var LeadSourceSchema = new mongoose.Schema({
4 | number: {
5 | type: String,
6 | required: true
7 | },
8 | description: {
9 | type: String,
10 | required: false
11 | },
12 | forwardingNumber: {
13 | type: String,
14 | required: false
15 | }
16 | });
17 |
18 | delete mongoose.models.LeadSource
19 |
20 | // Create a Mongoose model from our schema
21 | var LeadSource = mongoose.model('LeadSource', LeadSourceSchema);
22 |
23 | // export model as our module interface
24 | module.exports = LeadSource;
25 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "call-tracking-node",
3 | "version": "1.0.0",
4 | "description": "Call tracking using Twilio, TwiML and Express.js",
5 | "main": "index.js",
6 | "scripts": {
7 | "start": "node index",
8 | "test": "NODE_ENV=test ./node_modules/.bin/mocha"
9 | },
10 | "repository": {
11 | "type": "git",
12 | "url": "https://github.com/TwilioDevEd/call-tracking-node"
13 | },
14 | "keywords": [
15 | "twilio",
16 | "express",
17 | "mongodb",
18 | "twiml",
19 | "webhooks",
20 | "voip"
21 | ],
22 | "author": "Kevin Whinnery",
23 | "license": "MIT",
24 | "bugs": {
25 | "url": "https://github.com/TwilioDevEd/call-tracking-node/issues"
26 | },
27 | "homepage": "https://github.com/TwilioDevEd/call-tracking-node",
28 | "dependencies": {
29 | "body-parser": "^1.12.0",
30 | "connect-flash": "^0.1.1",
31 | "csurf": "^1.8.3",
32 | "dotenv-safe": "^3.0.0",
33 | "express": "^4.12.0",
34 | "express-session": "^1.10.3",
35 | "express-validator": "^2.17.1",
36 | "http-auth": "^3.1.0",
37 | "jade": "^1.9.2",
38 | "mongoose": "^4.11.0",
39 | "morgan": "^1.5.1",
40 | "q": "^1.4.1",
41 | "sanitize-html": "^1.10.0",
42 | "twilio": "~3.0.0-rc.16",
43 | "underscore": "^1.8.3"
44 | },
45 | "devDependencies": {
46 | "chai": "^3.5.0",
47 | "cheerio": "^0.22.0",
48 | "mocha": "^3.1.2",
49 | "nock": "^9.0.2",
50 | "sinon": "^1.16.1",
51 | "supertest": "^2.0.1"
52 | },
53 | "engines": {
54 | "node": ">=4.6.0"
55 | }
56 | }
57 |
--------------------------------------------------------------------------------
/public/404.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | HTTP Error 404: Page Not Found
5 |
6 |
7 | HTTP Error 404: Page Not Found
8 |
9 |
--------------------------------------------------------------------------------
/public/500.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | HTTP Error 500: Internal Server Error
5 |
6 |
7 | HTTP Error 500: Internal Server Error
8 |
9 |
--------------------------------------------------------------------------------
/public/css/main.css:
--------------------------------------------------------------------------------
1 | #main {
2 | padding-top:70px;
3 | }
4 |
5 | footer {
6 | border-top:1px solid #eee;
7 | padding:20px;
8 | margin:20px;
9 | text-align:center;
10 | }
11 |
12 | footer i {
13 | color:red;
14 | }
--------------------------------------------------------------------------------
/public/js/pieCharts.js:
--------------------------------------------------------------------------------
1 |
2 | $.getJSON('/lead/summary-by-lead-source', function(results) {
3 | results = _.map(_.zip(_.keys(results), _.values(results)), function(value) {
4 | return {
5 | description: value[0],
6 | lead_count: value[1]
7 | };
8 | });
9 |
10 | summaryByLeadSourceData = _.map(results, function(leadSourceDataPoint) {
11 | return {
12 | value: leadSourceDataPoint.lead_count,
13 | color: 'hsl(' + (180 * leadSourceDataPoint.lead_count/ results.length)
14 | + ', 100%, 50%)',
15 | label: leadSourceDataPoint.description
16 | };
17 | });
18 |
19 | var byLeadSourceContext = $("#pie-by-lead-source").get(0).getContext("2d");
20 | var byLeadSourceChart =
21 | new Chart(byLeadSourceContext).Pie(summaryByLeadSourceData);
22 | });
23 |
24 | $.getJSON('/lead/summary-by-city', function(results) {
25 | results = _.map(_.zip(_.keys(results), _.values(results)), function(value) {
26 | return {
27 | city: value[0],
28 | lead_count: value[1]
29 | };
30 | });
31 |
32 | summaryByCityData = _.map(results, function(cityDataPoint) {
33 | return {
34 | value: cityDataPoint.lead_count,
35 | color: 'hsl(' + (180 * cityDataPoint.lead_count/ results.length)
36 | + ', 100%, 50%)',
37 | label: cityDataPoint.city
38 | };
39 | });
40 |
41 | var byCityContext = $("#pie-by-city").get(0).getContext("2d");
42 | var byCityChart = new Chart(byCityContext).Pie(summaryByCityData);
43 | });
44 |
--------------------------------------------------------------------------------
/test/dashboardTest.js:
--------------------------------------------------------------------------------
1 | require('./testHelper');
2 |
3 | var cheerio = require('cheerio');
4 | var supertest = require('supertest');
5 | var expect = require('chai').expect;
6 |
7 | var app = require('../webapp');
8 | var config = require('../config');
9 | var LeadSource = require('../models/LeadSource');
10 | var agent = supertest(app);
11 |
12 | describe('Dashboard controllers', function() {
13 | after(function(done) {
14 | LeadSource.remove({}, done);
15 | });
16 |
17 | beforeEach(function(done) {
18 | LeadSource.remove({}, done);
19 | });
20 |
21 | describe('GET /dashboard', function() {
22 | it('shows a list of all lead sources', function() {
23 | var testNumber = '+1498324783';
24 | var testForwardingNumber = '+1982649248';
25 | var testDescription = 'A description here';
26 |
27 | var newLeadSource = new LeadSource({
28 | number: testNumber,
29 | forwardingNumber: testForwardingNumber,
30 | description: testDescription
31 | });
32 |
33 | return newLeadSource.save()
34 | .then(function() {
35 | return agent
36 | .get('/dashboard')
37 | .expect(200)
38 | .expect(function(response) {
39 | expect(response.text).to.contain(testNumber);
40 | expect(response.text).to.contain(testForwardingNumber);
41 | expect(response.text).to.contain(testDescription);
42 | });
43 | });
44 | });
45 | });
46 | });
47 |
--------------------------------------------------------------------------------
/test/leadSourcesTest.js:
--------------------------------------------------------------------------------
1 | require('./testHelper');
2 |
3 | var cheerio = require('cheerio');
4 | var supertest = require('supertest');
5 | var expect = require('chai').expect;
6 | var nock = require('nock');
7 | var app = require('../webapp');
8 | var config = require('../config');
9 | var LeadSource = require('../models/LeadSource');
10 |
11 | describe('Lead sources controllers', function() {
12 | after(function(done) {
13 | LeadSource.remove({}, done);
14 | });
15 |
16 | beforeEach(function(done) {
17 | LeadSource.remove({}, done);
18 | });
19 |
20 | describe('POST /lead-source', function() {
21 | it('saves the number after purchase', function() {
22 | var agent = supertest(app);
23 | var phoneNumberToPurchase = '+13153640102';
24 |
25 | nock('https://api.twilio.com')
26 | .post(`/2010-04-01/Accounts/${process.env.TWILIO_ACCOUNT_SID}/IncomingPhoneNumbers.json`)
27 | .reply(200, {
28 | phone_number: phoneNumberToPurchase,
29 | });
30 |
31 | return agent
32 | .post('/lead-source')
33 | .type('form')
34 | .send({
35 | phoneNumber: phoneNumberToPurchase
36 | })
37 | .expect(303)
38 | .expect(function(response) {
39 | return LeadSource.findOne({number: phoneNumberToPurchase})
40 | .then(function(found) {
41 | expect(response.headers.location)
42 | .to.equal('/lead-source/' + found._id + '/edit');
43 | });
44 | });
45 | });
46 | });
47 |
48 | describe('GET /lead-source/:id/edit', function() {
49 | it('displays existing values', function(done) {
50 | var agent = supertest(app);
51 | var phoneNumber = '+155555555';
52 | var forwardingNumber = '+177777777';
53 | var leadSource = new LeadSource({
54 | number: phoneNumber,
55 | description: 'Some description',
56 | forwardingNumber: forwardingNumber
57 | });
58 | leadSource.save().then(function() {
59 | agent
60 | .get('/lead-source/' + leadSource._id + '/edit')
61 | .expect(function(response) {
62 | var $ = cheerio.load(response.text);
63 | expect($('input#description')[0].attribs.value)
64 | .to.equal('Some description');
65 | expect($('input#forwardingNumber')[0].attribs.value)
66 | .to.equal(forwardingNumber);
67 | })
68 | .expect(200, done);
69 | });
70 | });
71 | });
72 |
73 | describe('POST /lead-source/:id/update', function() {
74 | it('validates a description and forwarding number', function(done) {
75 | var agent = supertest(app);
76 | var phoneNumber = '+155555555';
77 | var forwardingNumber = '+177777777';
78 | var leadSource = new LeadSource({
79 | number: phoneNumber,
80 | description: 'Some description',
81 | forwardingNumber: forwardingNumber
82 | });
83 |
84 | leadSource.save().then(function() {
85 | var updateUrl = '/lead-source/' + leadSource._id + '/update';
86 | var editUrl = '/lead-source/' + leadSource._id + '/edit';
87 | agent
88 | .post(updateUrl)
89 | .type('form')
90 | .send({
91 | description: '',
92 | forwardingNumber: ''
93 | })
94 | .expect(function(response) {
95 | expect(response.headers.location).to.equal(editUrl);
96 | })
97 | .expect(303, done);
98 | });
99 | });
100 |
101 | it('updates an existing lead source and redirects', function(done) {
102 | var agent = supertest(app);
103 | var phoneNumber = '+155555555';
104 | var newDescription = 'Some new description';
105 | var newForwardingNumber = '+177777777';
106 | var leadSource = new LeadSource({number: phoneNumber});
107 |
108 | leadSource.save().then(function() {
109 | var updateUrl = '/lead-source/' + leadSource._id + '/update';
110 | agent
111 | .post(updateUrl)
112 | .type('form')
113 | .send({
114 | description: newDescription,
115 | forwardingNumber: newForwardingNumber
116 | })
117 | .expect(function(response) {
118 | expect(response.headers.location).to.equal('/dashboard');
119 | var result = LeadSource.findOne({number: phoneNumber})
120 | result.then(function(foundLeadSource) {
121 | expect(foundLeadSource.description).to.equal(newDescription);
122 | expect(foundLeadSource.forwardingNumber)
123 | .to.equal(newForwardingNumber);
124 | });
125 | })
126 | .expect(303, done);
127 | });
128 | });
129 | });
130 | });
131 |
--------------------------------------------------------------------------------
/test/leadsTest.js:
--------------------------------------------------------------------------------
1 | require('./testHelper');
2 |
3 | var supertest = require('supertest');
4 | var expect = require('chai').expect;
5 | var q = require('q');
6 |
7 | var app = require('../webapp');
8 | var config = require('../config');
9 | var LeadSource = require('../models/LeadSource');
10 | var Lead = require('../models/Lead');
11 |
12 | describe('Lead controllers', function() {
13 | after(function(done) {
14 | Lead.remove({}).then(function() {
15 | LeadSource.remove({}, done);
16 | });
17 | });
18 |
19 | beforeEach(function(done) {
20 | Lead.remove({}).then(function() {
21 | LeadSource.remove({}, done);
22 | });
23 | });
24 |
25 | describe('POST /lead', function() {
26 | var agent = supertest(app);
27 | var callerNumber = '+1248623948';
28 | var callSid = '5up3runiqu3c411s1d';
29 | var city = 'Some city';
30 | var state = 'Some state';
31 | var callerName = 'Someone';
32 |
33 | var leadSourceNumber = '+149372894';
34 | var leadSourceDescription = 'Some description';
35 | var leadSourceForwardingNumber = '+132947845';
36 |
37 | it('forwards the call to a different number', function(done) {
38 | var newLeadSource = new LeadSource({
39 | number: leadSourceNumber,
40 | description: leadSourceDescription,
41 | forwardingNumber: leadSourceForwardingNumber
42 | });
43 | newLeadSource.save().then(function() {
44 | agent
45 | .post('/lead')
46 | .type('form')
47 | .send({
48 | From: callerNumber,
49 | To: leadSourceNumber,
50 | CallSid: callSid,
51 | FromCity: city,
52 | FromState: state,
53 | CallerName: callerName
54 | })
55 | .expect(function(response) {
56 | var twiml = ''
57 | + leadSourceForwardingNumber.toString()
58 | + '';
59 | expect(response.text).to.contain(twiml);
60 | })
61 | .expect(200, done);
62 | });
63 | });
64 |
65 | it('records a new lead', function(done) {
66 | var newLeadSource = new LeadSource({
67 | number: leadSourceNumber,
68 | description: leadSourceDescription,
69 | forwardingNumber: leadSourceForwardingNumber
70 | });
71 | newLeadSource.save().then(function(savedLeadSource) {
72 | agent
73 | .post('/lead')
74 | .type('form')
75 | .send({
76 | From: callerNumber,
77 | To: leadSourceNumber,
78 | CallSid: callSid,
79 | FromCity: city,
80 | FromState: state,
81 | CallerName: callerName
82 | })
83 | .expect(function(response) {
84 | Lead.findOne({}).then(function(newLead) {
85 | expect(newLead.callerNumber).to.equal(callerNumber);
86 | expect(newLead.leadSource.toString())
87 | .to.equal(savedLeadSource._id.toString());
88 | expect(newLead.city).to.equal(city);
89 | expect(newLead.state).to.equal(state);
90 | expect(newLead.callerName).to.equal(callerName);
91 | });
92 | })
93 | .expect(200, done);
94 | });
95 | });
96 | });
97 |
98 | describe('statistics for pie charts', function() {
99 | var leadSourceOneNumber = '+149372894';
100 | var leadSourceOneDescription = 'Some description';
101 | var leadSourceOneForwardingNumber = '+132947845';
102 |
103 | var leadSourceTwoNumber = '+149343243';
104 | var leadSourceTwoDescription = 'Some other description';
105 | var leadSourceTwoForwardingNumber = '+193274345';
106 |
107 | var callerNumber = '+1248623948';
108 | var callSid = '5up3runiqu3c411s1d';
109 |
110 | it('responds with stats for a leads by lead source chart', function(done) {
111 | var newLeadSourceOne = new LeadSource({
112 | number: leadSourceOneNumber,
113 | description: leadSourceOneDescription,
114 | forwardingNumber: leadSourceOneForwardingNumber
115 | }).save();
116 |
117 | var newLeadSourceTwo = new LeadSource({
118 | number: leadSourceTwoNumber,
119 | description: leadSourceTwoDescription,
120 | forwardingNumber: leadSourceTwoForwardingNumber
121 | }).save();
122 |
123 | var newLead = new Lead({
124 | callerNumber: callerNumber,
125 | callSid: callSid
126 | });
127 |
128 | q.all([
129 | newLeadSourceOne,
130 | newLeadSourceTwo
131 | ]).then(function(savedLeadSources) {
132 | var leadSourceOne = savedLeadSources[0];
133 | var leadSourceTwo = savedLeadSources[1];
134 |
135 | newLead.leadSource = leadSourceOne._id;
136 | return newLead.save();
137 | }).then(function(savedLead) {
138 | var agent = supertest(app);
139 | agent
140 | .get('/lead/summary-by-lead-source')
141 | .expect(function(response) {
142 | expect(response.text).to.equal('{"Some description":1}');
143 | })
144 | .expect(200, done);
145 | }).catch(function(err) {
146 | console.log(err);
147 | done();
148 | });
149 | });
150 |
151 | it('responds with statistics for a leads by city chart', function(done) {
152 | var newLeadSource = new LeadSource({
153 | number: leadSourceOneNumber,
154 | description: leadSourceOneDescription,
155 | forwardingNumber: leadSourceOneForwardingNumber
156 | }).save();
157 |
158 | var newLead = new Lead({
159 | callerNumber: callerNumber,
160 | callSid: callSid,
161 | city: 'The moon'
162 | });
163 |
164 | newLeadSource.then(function(savedLeadSource) {
165 | newLead.leadSource = savedLeadSource._id;
166 | return newLead.save();
167 | }).then(function(savedLead) {
168 | var agent = supertest(app);
169 | agent
170 | .get('/lead/summary-by-city')
171 | .expect(function(response) {
172 | expect(response.text).to.equal('{"The moon":1}');
173 | })
174 | .expect(200, done);
175 | });
176 | });
177 | });
178 |
179 | });
180 |
--------------------------------------------------------------------------------
/test/testHelper.js:
--------------------------------------------------------------------------------
1 | var mongoose = require('mongoose');
2 | mongoose.Promise = Promise;
3 |
4 | var connStr = 'mongodb://localhost/call-tracking-testdb';
5 |
6 | var conn;
7 |
8 | if (mongoose.connections.length == 0) {
9 | conn = mongoose.connect(connStr);
10 | } else {
11 | if (!mongoose.connections[0].host) {
12 | conn = mongoose.connect(connStr);
13 | }
14 | }
15 |
16 | exports.mongoConnection = conn;
17 |
--------------------------------------------------------------------------------
/util/twimlApp.js:
--------------------------------------------------------------------------------
1 | var twilio = require('twilio');
2 | var config = require('../config');
3 | var q = require('q');
4 |
5 | // Create reusable Twilio API client
6 | var client = twilio(config.apiKey, config.apiSecret, { accountSid: config.accountSid });
7 |
8 | var getTwimlAppSid = function(appNameToFind) {
9 | var appName = appNameToFind || 'Call tracking app';
10 |
11 | if (process.env.TWILIO_APP_SID) {
12 | return q(process.env.TWILIO_APP_SID);
13 | }
14 |
15 | return client.applications.list({
16 | friendlyName: appName
17 | }).then(function(results) {
18 | if (results.applications.length === 0) {
19 | throw new Error('No such app');
20 | } else {
21 | return results.applications[0].sid;
22 | }
23 | }).catch(function() {
24 | return client.applications.create({
25 | friendlyName: 'Call tracking app',
26 | voiceCallerIdLookup: true
27 | }).then(function(newApp) {
28 | return newApp.sid;
29 | });
30 | });
31 | };
32 |
33 | exports.getTwimlAppSid = getTwimlAppSid;
34 |
--------------------------------------------------------------------------------
/views/availableNumbers.jade:
--------------------------------------------------------------------------------
1 | extends layout
2 |
3 | block content
4 | .row
5 | .col-lg-4
6 | table.table
7 | thead
8 | tr
9 | th Phone number
10 | th State
11 | th
12 | tbody
13 | each number in availableNumbers
14 | tr
15 | td= number.friendlyName
16 | td= number.region
17 | td
18 | form(name='purchaseNumber', action='/lead-source', method='POST')
19 | input(type='hidden', name='phoneNumber', value="#{number.phoneNumber}")
20 | input(type='hidden', name='_csrf', value="#{csrftoken}")
21 | input(type='submit', value='Purchase').btn.btn-sm.btn-primary
22 |
--------------------------------------------------------------------------------
/views/dashboard.jade:
--------------------------------------------------------------------------------
1 | extends layout
2 |
3 | block content
4 | h2 Call Tracking
5 | .row
6 | .col-lg-4
7 | h3 Add a new number
8 | p.
9 | Create a new lead source by purchasing a new phone number.
10 | Area code is optional
11 |
12 | form(name='searchNumbers', action='/available-numbers', method='GET')
13 | .form-group
14 | label(for='areaCode') Area code
15 | input.form-control(type='number', name='areaCode')
16 | input.btn.btn-sm.btn-primary(type='submit', value='Search')
17 |
18 | hr
19 | h3 Lead sources
20 | a.btn.btn-sm.btn-default(
21 | href='https://www.twilio.com/console/voice/twiml/apps/' + '#{appSid}'
22 | target='_blank') App configuration
23 | table.table#leadSources
24 | thead
25 | tr
26 | th Phone number
27 | th Description
28 | th Forwarding number
29 | th
30 | tbody
31 | each leadSource in leadSources
32 | tr
33 | td #{leadSource.number}
34 | td #{leadSource.description}
35 | td #{leadSource.forwardingNumber}
36 | td
37 | a.btn.btn-xs.btn-default(
38 | href= '/lead-source/' + '#{leadSource._id}' + '/edit') Edit
39 |
40 | .col-lg-4
41 | h3 Calls by lead source
42 | p The number of incoming calls each lead source has received
43 | canvas#pie-by-lead-source
44 | .col-lg-4
45 | h3 Calls by city
46 | p.
47 | The number of incoming calls from different cities, based on Twilio call
48 | data
49 | canvas#pie-by-city
50 |
51 | block scripts
52 | script(src='//cdnjs.cloudflare.com/ajax/libs/underscore.js/1.8.3/underscore-min.js')
53 | script(src='//cdnjs.cloudflare.com/ajax/libs/Chart.js/1.0.2/Chart.min.js')
54 | script(src='js/pieCharts.js')
55 |
--------------------------------------------------------------------------------
/views/editLeadSource.jade:
--------------------------------------------------------------------------------
1 | extends layout
2 |
3 | block content
4 | h2 Edit a lead source
5 | hr
6 |
7 | if messages.length !== 0
8 | .alert.alert-danger
9 | ul
10 | each message in messages
11 | li.validationError
12 | | #{message.msg}
13 |
14 | h3 Lead source phone number: #{leadSourcePhoneNumber}
15 | .row
16 | .col-lg-4
17 | form(name='editLeadSource', action='/lead-source/#{leadSourceId}/update', method='POST')
18 | .form-group
19 | label(for='description') Description:
20 | input(type='text', name='description', value=leadSourceDescription).form-control#description
21 | .form-group
22 | label(for='forwardingNumber') Forwarding number:
23 | input(type='text', name='forwardingNumber', value=leadSourceForwardingNumber).form-control#forwardingNumber
24 | input(type='hidden', name='_csrf', value="#{csrftoken}")
25 | input(type='submit', value='Update').btn.btn-primary
26 |
--------------------------------------------------------------------------------
/views/layout.jade:
--------------------------------------------------------------------------------
1 | doctype html
2 | html(lang='en')
3 | head
4 | title= title || 'Call Tracking'
5 |
6 | // Twilio shortcut icons
7 | link(rel='shortcut icon',
8 | href='//twilio.com/bundles/marketing/img/favicons/favicon.ico')
9 | link(rel='apple-touch-icon',
10 | href='//twilio.com/bundles/marketing/img/favicons/favicon_57.png')
11 | link(rel='apple-touch-icon', sizes='72x72',
12 | href='/bundles/marketing/img/favicons/favicon_72.png')
13 | link(rel='apple-touch-icon' sizes='114x114'
14 | href='//twilio.com/bundles/marketing/img/favicons/favicon_114.png')
15 |
16 | // Include Font Awesome Icons
17 | link(rel='stylesheet',
18 | href='//maxcdn.bootstrapcdn.com/font-awesome/4.2.0/css/font-awesome.min.css')
19 |
20 | // Twitter Bootstrap included for some basic styling
21 | link(rel='stylesheet',
22 | href='//maxcdn.bootstrapcdn.com/bootstrap/3.2.0/css/bootstrap.min.css')
23 |
24 | // HTML 5 shims for older IE
25 |
29 |
30 | link(rel='stylesheet', href='/css/main.css')
31 |
32 | //- Include any page-specific styles
33 | block styles
34 |
35 | body
36 | // Bootstrap-powered nav bar
37 | nav.navbar.navbar-default.navbar-fixed-top
38 | .container
39 | .navbar-header
40 | a.navbar-brand(href='/dashboard') Call Tracking
41 |
42 | //- Include page content
43 | #main.container
44 | block content
45 |
46 | footer.container.
47 | Made with by your pals
48 | @twilio.
49 |
50 | // Include jQuery and Bootstrap scripts
51 | script(src='//ajax.googleapis.com/ajax/libs/jquery/1.11.2/jquery.min.js')
52 | script(src='//maxcdn.bootstrapcdn.com/bootstrap/3.3.2/js/bootstrap.min.js')
53 |
54 | //- Include any page-specific scripts
55 | block scripts
--------------------------------------------------------------------------------
/webapp.js:
--------------------------------------------------------------------------------
1 | var path = require('path');
2 | var express = require('express');
3 | var bodyParser = require('body-parser');
4 | var expressValidator = require('express-validator');
5 | var session = require('express-session');
6 | var flash = require('connect-flash');
7 | var morgan = require('morgan');
8 | var csurf = require('csurf');
9 | var config = require('./config');
10 | var mongoose = require('mongoose');
11 |
12 | mongoose.Promise = Promise;
13 |
14 | // Create Express web app
15 | var app = express();
16 | app.set('view engine', 'jade');
17 |
18 | // Use morgan for HTTP request logging in dev and prod
19 | if (process.env.NODE_ENV !== 'test') {
20 | app.use(morgan('combined'));
21 | }
22 |
23 | // Serve static assets
24 | app.use(express.static(path.join(__dirname, 'public')));
25 |
26 | // Parse incoming form-encoded HTTP bodies
27 | app.use(bodyParser.urlencoded({
28 | extended: true
29 | }));
30 |
31 | // Validate requests
32 | app.use(expressValidator());
33 |
34 | // Create and manage HTTP sessions for all requests
35 | app.use(session({
36 | secret: config.secret,
37 | resave: true,
38 | saveUninitialized: true
39 | }));
40 |
41 | // Use connect-flash to persist informational messages across redirects
42 | app.use(flash());
43 |
44 | // Configure application routes
45 | var routes = require('./controllers/router');
46 | var webRouter = express.Router();
47 | var webhookRouter = express.Router();
48 |
49 | // Add CSRF protection for web routes
50 | if (process.env.NODE_ENV !== 'test') {
51 | webRouter.use(csurf());
52 | webRouter.use(function(request, response, next) {
53 | response.locals.csrftoken = request.csrfToken();
54 | next();
55 | });
56 | }
57 |
58 | routes.webhookRoutes(webhookRouter);
59 | routes.webRoutes(webRouter);
60 |
61 | app.use(webhookRouter);
62 | app.use(webRouter);
63 |
64 | // Handle 404
65 | app.use(function(request, response, next) {
66 | response.status(404);
67 | response.sendFile(path.join(__dirname, 'public', '404.html'));
68 | });
69 |
70 | // Unhandled errors (500)
71 | app.use(function(err, request, response, next) {
72 | console.error('An application error has occurred:');
73 | console.error(err);
74 | console.error(err.stack);
75 | response.status(500);
76 | response.sendFile(path.join(__dirname, 'public', '500.html'));
77 | });
78 |
79 | // Export Express app
80 | module.exports = app;
81 |
--------------------------------------------------------------------------------