├── .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 | Twilio 3 | 4 | 5 | # Call tracking 6 | 7 | [![Node.js CI](https://github.com/TwilioDevEd/call-tracking-node/actions/workflows/node.js.yml/badge.svg)](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 | ![app configuration button screenshot](images/app-configuration.png) 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 | ![webhook configuration](images/webhook.png) 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 | ![phone number search](images/phone-search.png) 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 | ![available numbers view](images/purchase-number.png) 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 | ![available numbers view](images/setup-lead.png) 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 | ![main dashboard view](images/main-dashboard.png) 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 | --------------------------------------------------------------------------------