├── .gitignore ├── LICENSE ├── Procfile ├── README.md ├── app.json ├── config.js ├── controllers ├── messages.js └── router.js ├── index.js ├── middleware └── basic-auth.js ├── models └── Message.js ├── package.json ├── public ├── 404.html ├── 500.html └── css │ └── main.css ├── test └── functional.js ├── views ├── layout.jade └── messages.jade └── webapp.js /.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 -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TwilioDevEd/starter-node-express/c52bd9fcb4e70e28b8bbf7c7078e0af873847351/LICENSE -------------------------------------------------------------------------------- /Procfile: -------------------------------------------------------------------------------- 1 | web: node index.js -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Twilio Starter Project for Node.js and Express 2 | 3 | This project is intended to jump-start Twilio development projects using Node.js 4 | and the [Express](http://www.expressjs.com) web framework. In addition to 5 | Express, this project contains other third-party modules that may be useful in 6 | creating Node.js web applications generally. 7 | 8 | This sample application demonstrates how to receive incoming SMS messages and 9 | store them in a MongoDB database. 10 | 11 | ## Running the Project on Your Machine 12 | 13 | To run this project on your computer, download or clone the source. You will 14 | also need to download and install either [Node.js](http://nodejs.org/) 15 | or [io.js](https://iojs.org/en/index.html), both of which should also install 16 | [npm](https://www.npmjs.com/). 17 | 18 | You will also need to [sign up for a Twilio account](https://www.twilio.com/try-twilio) 19 | if you don't have one already. 20 | 21 | ### Install Dependencies 22 | 23 | Navigate to the project directory in your terminal and run: 24 | 25 | ```bash 26 | npm install 27 | ``` 28 | 29 | This should install all of our project dependencies from npm into a local 30 | `node_modules` folder. 31 | 32 | ### Configuration 33 | 34 | Next, open `config.js` at the root of the project and update it with values 35 | from your environment and your [Twilio account](https://www.twilio.com/user/account/voice-messaging). 36 | You can either export these values as system environment variables (this is the 37 | default setup), or you can replace these values with hard-coded strings 38 | (be careful you don't commit them to git!). 39 | 40 | This sample application stores data in a MongoDB database using 41 | [Mongoose](http://mongoosejs.com/). You can download and run MongoDB 42 | yourself ([OS X](http://docs.mongodb.org/manual/tutorial/install-mongodb-on-os-x/), 43 | [Linux](http://docs.mongodb.org/manual/tutorial/install-mongodb-on-ubuntu/), 44 | [Windows](http://docs.mongodb.org/manual/tutorial/install-mongodb-on-windows/)), 45 | or you can use a hosted service like [compose.io](https://www.compose.io/). 46 | 47 | On OS X, the maybe the easiest way to get MongoDB running locally is to install 48 | via [Homebrew](http://brew.sh/). 49 | 50 | ```bash 51 | brew install mongodb 52 | ``` 53 | 54 | You should then be able to run a local server with: 55 | 56 | ```bash 57 | mongod 58 | ``` 59 | 60 | By default, there will be a local database running that's not password protected. 61 | To connect to MongoDB, you'll need a [connection string](http://mongoosejs.com/docs/connections.html) 62 | to use with Mongoose. Enter this into your current terminal window, and/or consider 63 | adding it to your `.bash_profile` so every new terminal window will have this 64 | setting. 65 | 66 | ```bash 67 | export MONGO_URL=mongodb://localhost/starter_node_express 68 | ``` 69 | 70 | ### Running the Project 71 | 72 | To launch the application, you can use `node .` in the project's root directory. 73 | You might also consider using [nodemon](https://github.com/remy/nodemon) for 74 | this. It works just like the node command, but automatically restarts your 75 | application when you change any source code files. 76 | 77 | ```bash 78 | npm install -g nodemon 79 | nodemon . 80 | ``` 81 | 82 | ### Running Tests 83 | 84 | Basic functional tests (requires local MongoDB) can be run with: 85 | 86 | ```bash 87 | npm test 88 | ``` 89 | 90 | ### Exposing Webhooks to Twilio 91 | 92 | To test your application locally with a Twilio number, we recommend using 93 | [ngrok](https://ngrok.com/docs). Use ngrok to expose a local port and get a 94 | publicly accessible URL you can use to accept incoming calls or texts to your 95 | Twilio numbers. 96 | 97 | The following example would expose your local Node application running on port 98 | 3000 at `http://chunky-danger-monkey.ngrok.io` (note that *reserved* subdomains 99 | are a paid feature of ngrok): 100 | 101 | ```bash 102 | ngrok http -subdomain=chunky-danger-monkey 3000 103 | ``` 104 | 105 | In your Twilio number configuration, you would then need to add `/calls` as the 106 | Voice URL route, and `/messages` as the Messaging URL. 107 | 108 | ## License 109 | 110 | MIT 111 | -------------------------------------------------------------------------------- /app.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Node.js/Express Starter", 3 | "description": "Starter Application for Node.js", 4 | "keywords": [ 5 | "twilio", 6 | "node.js", 7 | "mongodb", 8 | "mongoose", 9 | "express" 10 | ], 11 | "env": { 12 | "TWILIO_ACCOUNT_SID": { 13 | "description": "API username - find your account SID at https://www.twilio.com/user/account/voice-messaging", 14 | "required": true 15 | }, 16 | "TWILIO_AUTH_TOKEN": { 17 | "description": "API password - find your auth token at https://www.twilio.com/user/account/voice-messaging", 18 | "required": true 19 | }, 20 | "TWILIO_NUMBER": { 21 | "description": "A number in your Twilio account to use with this application in E.164 format (e.g. +16518675309) - view your numbers at https://www.twilio.com/user/account/phone-numbers/incoming", 22 | "required": true 23 | }, 24 | "MY_NUMBER": { 25 | "description": "Your own mobile phone number! The app will be calling and texting you. Specify in E.164 format (e.g. +16519991111)", 26 | "required": false 27 | }, 28 | "HTTP_BASIC_USERNAME": { 29 | "description": "HTTP Basic Auth username to log into this demo app (default: admin)", 30 | "required": false 31 | }, 32 | "HTTP_BASIC_PASSWORD": { 33 | "description": "HTTP Basic Auth username to log into this demo app (default: password)", 34 | "required": false 35 | } 36 | }, 37 | "website": "https://github.com/TwilioDevEd/starter-node-express", 38 | "repository": "https://github.com/TwilioDevEd/starter-node-express", 39 | "logo": "https://s3.amazonaws.com/howtodocs/twilio-logo.png", 40 | "success_url": "/", 41 | "addons": [ 42 | "mongolab" 43 | ] 44 | } 45 | -------------------------------------------------------------------------------- /config.js: -------------------------------------------------------------------------------- 1 | var cfg = {}; 2 | 3 | // HTTP Port to run our web application 4 | cfg.port = process.env.PORT || 3000; 5 | 6 | // HTTP Basic auth config, for light weight security on our demo app 7 | cfg.basic = { 8 | username: process.env.HTTP_BASIC_USERNAME || 'admin', 9 | password: process.env.HTTP_BASIC_PASSWORD || 'password' 10 | }; 11 | 12 | // A random string that will help generate secure one-time passwords and 13 | // HTTP sessions 14 | cfg.secret = process.env.APP_SECRET || 'keyboard cat'; 15 | 16 | // Your Twilio account SID and auth token, both found at: 17 | // https://www.twilio.com/user/account 18 | // 19 | // A good practice is to store these string values as system environment 20 | // variables, and load them from there as we are doing below. Alternately, 21 | // you could hard code these values here as strings. 22 | cfg.accountSid = process.env.TWILIO_ACCOUNT_SID; 23 | cfg.authToken = process.env.TWILIO_AUTH_TOKEN; 24 | 25 | // A Twilio number you control - choose one from: 26 | // https://www.twilio.com/user/account/phone-numbers/incoming 27 | // Specify in E.164 format, e.g. "+16519998877" 28 | cfg.twilioNumber = process.env.TWILIO_NUMBER; 29 | 30 | // Your own mobile phone number! The app may be calling and texting you to 31 | // test things out. Specify in E.164 format, e.g. "+16519998877" 32 | cfg.myNumber = process.env.MY_NUMBER; 33 | 34 | // MongoDB connection string - MONGO_URL is for local dev, 35 | // MONGOLAB_URI is for the MongoLab add-on for Heroku deployment 36 | cfg.mongoUrl = process.env.MONGOLAB_URI || process.env.MONGO_URL 37 | 38 | // Export configuration object 39 | module.exports = cfg; -------------------------------------------------------------------------------- /controllers/messages.js: -------------------------------------------------------------------------------- 1 | var sanitizeHtml = require('sanitize-html'); 2 | var MessagingResponse = require('twilio').twiml.MessagingResponse; 3 | var Message = require('../models/Message'); 4 | 5 | // create a new message when Twilio sends us a text 6 | exports.create = function(request, response) { 7 | // Create a new Message from the Twilio request 8 | var msg = new Message({ 9 | from: request.body.From, 10 | body: sanitizeHtml(request.body.Body) 11 | }); 12 | 13 | // save it, and return a response 14 | msg.save(function(err) { 15 | if (err) return reply('Oops, there was a problem.'); 16 | 17 | // Otherwise, confirm their message has been received 18 | reply('Message has been received!'); 19 | }); 20 | 21 | // return a TwiML response 22 | function reply(message) { 23 | var twiml = new MessagingResponse(); 24 | twiml.message(message); 25 | response.type('text/xml'); 26 | response.send(twiml.toString()); 27 | } 28 | }; 29 | 30 | // List Message records in our database 31 | exports.list = function(request, response) { 32 | // Use a custom query to get all the messages in a specific order 33 | Message.listAll(function(err, docs) { 34 | // Render a Jade template with all the message data 35 | response.render('messages', { 36 | messages: docs 37 | }); 38 | }); 39 | }; 40 | -------------------------------------------------------------------------------- /controllers/router.js: -------------------------------------------------------------------------------- 1 | var messages = require('./messages'); 2 | var basic = require('../middleware/basic-auth'); 3 | 4 | // Map routes to controller functions 5 | module.exports = function(app) { 6 | // Routes which handle creating messages (via Twilio) and listing them on 7 | // a simple web page (protected with HTTP basic auth) 8 | app.post('/messages', messages.create); 9 | app.get('/messages', basic, messages.list); 10 | }; -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | var http = require('http'); 2 | var mongoose = require('mongoose'); 3 | var config = require('./config'); 4 | 5 | // Initialize database connection - throws if database connection can't be 6 | // established 7 | mongoose.connect(config.mongoUrl); 8 | 9 | // Create Express web app 10 | var app = require('./webapp'); 11 | 12 | // Create an HTTP server and listen on the configured port 13 | var server = http.createServer(app); 14 | server.listen(config.port, function() { 15 | console.log('Express server listening on *:' + config.port); 16 | }); -------------------------------------------------------------------------------- /middleware/basic-auth.js: -------------------------------------------------------------------------------- 1 | var auth = require('http-auth'); 2 | var appInfo = require('../package.json'); 3 | var config = require('../config'); 4 | var USER = config.basic.username; 5 | var PASS = config.basic.password; 6 | 7 | // HTTP Basic auth configuration 8 | var basic = auth.basic({ 9 | realm: appInfo.name 10 | }, function(username, password, callback) { 11 | // test against configured username/password 12 | callback(username === USER && password === PASS); 13 | }); 14 | 15 | // Create Connect middleware from basic auth config 16 | module.exports = auth.connect(basic); -------------------------------------------------------------------------------- /models/Message.js: -------------------------------------------------------------------------------- 1 | var mongoose = require('mongoose'); 2 | 3 | var MessageSchema = new mongoose.Schema({ 4 | // phone number of sender 5 | from: { 6 | type: String, 7 | required: true 8 | }, 9 | 10 | // the text of their message 11 | body: { 12 | type: String, 13 | required: true 14 | }, 15 | 16 | // date the message was created 17 | date: { 18 | type: Date, 19 | default: Date.now 20 | } 21 | }); 22 | 23 | // Find all the messages sent so far, ordered by date 24 | MessageSchema.statics.listAll = function(callback) { 25 | Message.find().sort('-date').exec(callback); 26 | }; 27 | 28 | // Create a Mongoose model from our schema 29 | var Message = mongoose.model('Message', MessageSchema); 30 | 31 | // export model as our module interface 32 | module.exports = Message; -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "starter-node-express", 3 | "version": "1.0.0", 4 | "description": "A Twilio starter project for Node.js development using the Express web framework", 5 | "main": "index.js", 6 | "scripts": { 7 | "test": "export NODE_ENV=test && mocha test" 8 | }, 9 | "repository": { 10 | "type": "git", 11 | "url": "https://github.com/TwilioDevEd/starter-node-express" 12 | }, 13 | "keywords": [ 14 | "twilio", 15 | "express", 16 | "mongodb", 17 | "twiml", 18 | "webhooks", 19 | "voip" 20 | ], 21 | "author": "Kevin Whinnery", 22 | "license": "MIT", 23 | "bugs": { 24 | "url": "https://github.com/TwilioDevEd/starter-node-express/issues" 25 | }, 26 | "homepage": "https://github.com/TwilioDevEd/starter-node-express", 27 | "dependencies": { 28 | "body-parser": "^1.12.0", 29 | "connect-flash": "^0.1.1", 30 | "express": "^4.12.0", 31 | "express-session": "^1.10.3", 32 | "http-auth": "^2.2.5", 33 | "jade": "^1.9.2", 34 | "mongoose": "^4.1.5", 35 | "morgan": "^1.5.1", 36 | "sanitize-html": "^1.10.0", 37 | "twilio": "^3.0.0-rc.16" 38 | }, 39 | "devDependencies": { 40 | "chai": "^2.1.0", 41 | "cheerio": "^0.19.0", 42 | "mocha": "^2.3.0", 43 | "sinon": "^1.16.1", 44 | "supertest": "^1.1.0" 45 | }, 46 | "engines" : { "node" : ">=6.x" } 47 | } 48 | -------------------------------------------------------------------------------- /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 | } -------------------------------------------------------------------------------- /test/functional.js: -------------------------------------------------------------------------------- 1 | var cheerio = require('cheerio'); 2 | var supertest = require('supertest'); 3 | var mongoose = require('mongoose'); 4 | var expect = require('chai').expect; 5 | var app = require('../webapp'); 6 | var config = require('../config'); 7 | var Message = require('../models/Message'); 8 | 9 | describe('Messages web application', function() { 10 | 11 | // Create supertest agent to test our routes 12 | var agent = supertest(app); 13 | 14 | // Create a MongoDB connection and clear out messages collection before 15 | // running tests 16 | before(function(done) { 17 | mongoose.connect('mongodb://127.0.0.1/test'); 18 | Message.remove({}, done); 19 | }); 20 | 21 | // Test creating a message 22 | describe('POST /messages', function() { 23 | it('should require both a from number and a body', function(done) { 24 | agent.post('/messages').expect(200).end(function(err, response) { 25 | var $ = cheerio.load(response.text); 26 | expect($('Message').text()) 27 | .to.equal('Oops, there was a problem.'); 28 | done(); 29 | }); 30 | }); 31 | 32 | it('should create a message', function(done) { 33 | agent.post('/messages') 34 | .type('form') 35 | .send({ 36 | From: '+15556667777', 37 | Body: 'hello world' 38 | }) 39 | .expect(200) 40 | .end(function(err, response) { 41 | var $ = cheerio.load(response.text); 42 | expect($('Message').text()) 43 | .to.equal('Message has been received!'); 44 | done(); 45 | }); 46 | }); 47 | }); 48 | 49 | describe('GET /messages', function() { 50 | it('should require HTTP basic authentication', function(done) { 51 | agent.get('/messages').expect(401, done); 52 | }); 53 | 54 | it('should display a table containing one message', function(done) { 55 | agent.get('/messages') 56 | .auth(config.basic.username, config.basic.password) 57 | .expect(200) 58 | .end(function(err, response) { 59 | var $ = cheerio.load(response.text); 60 | expect($('tr').length).to.equal(2); 61 | done(); 62 | }); 63 | }); 64 | }); 65 | }); -------------------------------------------------------------------------------- /views/layout.jade: -------------------------------------------------------------------------------- 1 | doctype html 2 | html(lang='en') 3 | head 4 | title= title || 'Twilio Starter Project for Node.js and Express' 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 | link(rel='stylesheet', 24 | href='//maxcdn.bootstrapcdn.com/bootstrap/3.2.0/css/bootstrap-theme.min.css') 25 | 26 | // HTML 5 shims for older IE 27 | 31 | 32 | link(rel='stylesheet', href='/css/main.css') 33 | 34 | //- Include any page-specific styles 35 | block styles 36 | 37 | body 38 | // Bootstrap-powered nav bar 39 | nav.navbar.navbar-default.navbar-fixed-top 40 | .container 41 | .navbar-header 42 | a.navbar-brand(href='/messages') Messages 43 | 44 | //- Include page content 45 | #main.container 46 | block content 47 | 48 | footer.container. 49 | Made with by your pals 50 | @twilio. 51 | 52 | // Include jQuery and Bootstrap scripts 53 | script(src='//ajax.googleapis.com/ajax/libs/jquery/1.11.2/jquery.min.js') 54 | script(src='//maxcdn.bootstrapcdn.com/bootstrap/3.3.2/js/bootstrap.min.js') 55 | 56 | //- Include any page-specific scripts 57 | block scripts -------------------------------------------------------------------------------- /views/messages.jade: -------------------------------------------------------------------------------- 1 | extends layout 2 | 3 | block content 4 | table.table 5 | tr 6 | th From 7 | th Message Body 8 | each message in messages 9 | tr 10 | td= message.from 11 | td= message.body 12 | -------------------------------------------------------------------------------- /webapp.js: -------------------------------------------------------------------------------- 1 | var path = require('path'); 2 | var express = require('express'); 3 | var bodyParser = require('body-parser'); 4 | var session = require('express-session'); 5 | var flash = require('connect-flash'); 6 | var morgan = require('morgan'); 7 | var config = require('./config'); 8 | 9 | // Create Express web app 10 | var app = express(); 11 | app.set('view engine', 'jade'); 12 | 13 | // Use morgan for HTTP request logging in dev and prod 14 | if (process.env.NODE_ENV !== 'test') { 15 | app.use(morgan('combined')); 16 | } 17 | 18 | // Serve static assets 19 | app.use(express.static(path.join(__dirname, 'public'))); 20 | 21 | // Parse incoming form-encoded HTTP bodies 22 | app.use(bodyParser.urlencoded({ 23 | extended: true 24 | })); 25 | 26 | // Create and manage HTTP sessions for all requests 27 | app.use(session({ 28 | secret: config.secret, 29 | resave: true, 30 | saveUninitialized: true 31 | })); 32 | 33 | // Use connect-flash to persist informational messages across redirects 34 | app.use(flash()); 35 | 36 | // Configure application routes 37 | require('./controllers/router')(app); 38 | 39 | // Handle 404 40 | app.use(function (request, response, next) { 41 | response.status(404); 42 | response.sendFile(path.join(__dirname, 'public', '404.html')); 43 | }); 44 | 45 | // Unhandled errors (500) 46 | app.use(function(err, request, response, next) { 47 | console.error('An application error has occurred:'); 48 | console.error(err); 49 | console.error(err.stack); 50 | response.status(500); 51 | response.sendFile(path.join(__dirname, 'public', '500.html')); 52 | }); 53 | 54 | // Export Express app 55 | module.exports = app; --------------------------------------------------------------------------------