Repository: passport/express-4.x-facebook-example Branch: master Commit: a7cdd5a0d903 Files: 19 Total size: 34.7 KB Directory structure: gitextract_mcj4yqm1/ ├── .github/ │ └── FUNDING.yml ├── .gitignore ├── LICENSE ├── README.md ├── app.js ├── bin/ │ └── www ├── db.js ├── package.json ├── public/ │ └── css/ │ ├── app.css │ ├── base.css │ ├── home.css │ ├── index.css │ └── login.css ├── routes/ │ ├── auth.js │ └── index.js └── views/ ├── error.ejs ├── home.ejs ├── index.ejs └── login.ejs ================================================ FILE CONTENTS ================================================ ================================================ FILE: .github/FUNDING.yml ================================================ github: jaredhanson ================================================ FILE: .gitignore ================================================ .env var # Node.js node_modules/ npm-debug.log* # Mac OS X .DS_Store ================================================ FILE: LICENSE ================================================ This is free and unencumbered software released into the public domain. Anyone is free to copy, modify, publish, use, compile, sell, or distribute this software, either in source code form or as a compiled binary, for any purpose, commercial or non-commercial, and by any means. In jurisdictions that recognize copyright laws, the author or authors of this software dedicate any and all copyright interest in the software to the public domain. We make this dedication for the benefit of the public at large and to the detriment of our heirs and successors. We intend this dedication to be an overt act of relinquishment in perpetuity of all present and future rights to this software under copyright law. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. For more information, please refer to ================================================ FILE: README.md ================================================ This example demonstrates how to use [Express](https://expressjs.com) 4.x and [Passport](https://www.passportjs.org) to log users in with [Facebook](https://www.facebook.com). Use this example as a starting point for your own web applications. ## Quick Start To get started with this example, clone the repository and install the dependencies. ```bash $ git clone git@github.com:passport/express-4.x-facebook-example.git $ cd express-4.x-facebook-example $ npm install ``` This example requires credentials from Facebook, which can be obtained by [creating](https://developers.facebook.com/docs/development/create-an-app) an app in the [App Dashboard](https://developers.facebook.com/apps). The OAuth redirect URI of the app should be set to: `http://localhost:3000/oauth2/redirect/www.facebook.com` Once credentials have been obtained, create a `.env` file and add the following environment variables: ``` FACEBOOK_CLIENT_ID={{INSERT_APP_ID_HERE}} FACEBOOK_CLIENT_SECRET={{INSERT_APP_SECRET_HERE}} ``` Start the server. ```bash $ npm start ``` Navigate to [`http://localhost:3000`](http://localhost:3000). ## Overview This example illustrates how to use [Passport](https://www.passportjs.org) and the [`passport-facebook`](https://www.passportjs.org/packages/passport-facebook/) strategy within an [Express](https://expressjs.com) application to log users in with [Facebook](https://www.facebook.com). The example builds upon the scaffolding created by [Express generator](https://expressjs.com/en/starter/generator.html), and uses [EJS](https://ejs.co) as a view engine and plain CSS for styling. This scaffolding was generated by executing: ``` $ express --view ejs express-4.x-facebook-example ``` The example uses [SQLite](https://www.sqlite.org) for storing user accounts. SQLite is a lightweight database that works well for development, including this example. Added to the scaffolding are files which add authentication to the application. * [`boot/db.js`](boot/db.js) This file initializes the database by creating the tables used to store user accounts and credentials. * [`boot/auth.js`](boot/auth.js) This file initializes Passport. It configures the Facebook strategy and supplies the serialization functions used for session management. * [`routes/auth.js`](routes/auth.js) This file defines the routes used for authentication. In particular, there are three routes used to authenticate with Facebook: - `GET /login` This route renders a page that prompts the user to login with Facebook. - `GET /login/federated/www.facebook.com` This route begins the authentication sequence by redirecting the user to Facebook. - `POST /oauth2/redirect/www.facebook.com` This route completes the authentication sequence when Facebook redirects the user back to the application. When a new user logs in, a user account is automatically created and their Facebook account is linked. When an existing user returns, they are logged in to their linked account. ## License [The Unlicense](https://opensource.org/licenses/unlicense) ================================================ FILE: app.js ================================================ require('dotenv').config(); var createError = require('http-errors'); var express = require('express'); var path = require('path'); var cookieParser = require('cookie-parser'); var session = require('express-session'); var csrf = require('csurf'); var passport = require('passport'); var logger = require('morgan'); // pass the session to the connect sqlite3 module // allowing it to inherit from session.Store var SQLiteStore = require('connect-sqlite3')(session); var indexRouter = require('./routes/index'); var authRouter = require('./routes/auth'); var app = express(); // view engine setup app.set('views', path.join(__dirname, 'views')); app.set('view engine', 'ejs'); app.locals.pluralize = require('pluralize'); app.use(logger('dev')); app.use(express.json()); app.use(express.urlencoded({ extended: false })); app.use(cookieParser()); app.use(express.static(path.join(__dirname, 'public'))); app.use(session({ secret: 'keyboard cat', resave: false, // don't save session if unmodified saveUninitialized: false, // don't create session until something stored store: new SQLiteStore({ db: 'sessions.db', dir: './var/db' }) })); app.use(csrf()); app.use(passport.authenticate('session')); app.use(function(req, res, next) { var msgs = req.session.messages || []; res.locals.messages = msgs; res.locals.hasMessages = !! msgs.length; req.session.messages = []; next(); }); app.use(function(req, res, next) { res.locals.csrfToken = req.csrfToken(); next(); }); app.use('/', indexRouter); app.use('/', authRouter); // catch 404 and forward to error handler app.use(function(req, res, next) { next(createError(404)); }); // error handler app.use(function(err, req, res, next) { // set locals, only providing error in development res.locals.message = err.message; res.locals.error = req.app.get('env') === 'development' ? err : {}; // render the error page res.status(err.status || 500); res.render('error'); }); module.exports = app; ================================================ FILE: bin/www ================================================ #!/usr/bin/env node /** * Module dependencies. */ var app = require('../app'); var debug = require('debug')('todos:server'); var http = require('http'); /** * Get port from environment and store in Express. */ var port = normalizePort(process.env.PORT || '3000'); app.set('port', port); /** * Create HTTP server. */ var server = http.createServer(app); /** * Listen on provided port, on all network interfaces. */ server.listen(port); server.on('error', onError); server.on('listening', onListening); /** * Normalize a port into a number, string, or false. */ function normalizePort(val) { var port = parseInt(val, 10); if (isNaN(port)) { // named pipe return val; } if (port >= 0) { // port number return port; } return false; } /** * Event listener for HTTP server "error" event. */ function onError(error) { if (error.syscall !== 'listen') { throw error; } var bind = typeof port === 'string' ? 'Pipe ' + port : 'Port ' + port; // handle specific listen errors with friendly messages switch (error.code) { case 'EACCES': console.error(bind + ' requires elevated privileges'); process.exit(1); break; case 'EADDRINUSE': console.error(bind + ' is already in use'); process.exit(1); break; default: throw error; } } /** * Event listener for HTTP server "listening" event. */ function onListening() { var addr = server.address(); var bind = typeof addr === 'string' ? 'pipe ' + addr : 'port ' + addr.port; debug('Listening on ' + bind); } ================================================ FILE: db.js ================================================ var sqlite3 = require('sqlite3'); var mkdirp = require('mkdirp'); mkdirp.sync('var/db'); var db = new sqlite3.Database('var/db/todos.db'); db.serialize(function() { db.run("CREATE TABLE IF NOT EXISTS users ( \ id INTEGER PRIMARY KEY, \ username TEXT UNIQUE, \ hashed_password BLOB, \ salt BLOB, \ name TEXT, \ email TEXT UNIQUE, \ email_verified INTEGER \ )"); db.run("CREATE TABLE IF NOT EXISTS federated_credentials ( \ id INTEGER PRIMARY KEY, \ user_id INTEGER NOT NULL, \ provider TEXT NOT NULL, \ subject TEXT NOT NULL, \ UNIQUE (provider, subject) \ )"); db.run("CREATE TABLE IF NOT EXISTS todos ( \ id INTEGER PRIMARY KEY, \ owner_id INTEGER NOT NULL, \ title TEXT NOT NULL, \ completed INTEGER \ )"); }); module.exports = db; ================================================ FILE: package.json ================================================ { "name": "todos-express-facebook", "version": "0.0.0", "private": true, "description": "Todo app using Express and Passport for log in with Facebook.", "keywords": [ "example", "express", "passport", "sqlite" ], "author": { "name": "Jared Hanson", "email": "jaredhanson@gmail.com", "url": "https://www.jaredhanson.me/" }, "homepage": "https://github.com/passport/todos-express-facebook", "repository": { "type": "git", "url": "git://github.com/passport/todos-express-facebook.git" }, "bugs": { "url": "https://github.com/passport/todos-express-facebook/issues" }, "funding": { "type": "github", "url": "https://github.com/sponsors/jaredhanson" }, "license": "Unlicense", "scripts": { "start": "node ./bin/www" }, "dependencies": { "connect-ensure-login": "^0.1.1", "connect-sqlite3": "^0.9.13", "cookie-parser": "~1.4.4", "csurf": "^1.11.0", "debug": "~2.6.9", "dotenv": "^8.6.0", "ejs": "~2.6.1", "express": "~4.16.1", "express-session": "^1.17.2", "http-errors": "~1.6.3", "mkdirp": "^1.0.4", "morgan": "~1.9.1", "passport": "^0.6.0", "passport-facebook": "^3.0.0", "pluralize": "^8.0.0", "sqlite3": "^5.0.2" } } ================================================ FILE: public/css/app.css ================================================ .nav { position: absolute; top: -130px; right: 0; } .nav ul { margin: 0; list-style: none; text-align: center; } .nav li { display: inline-block; height: 40px; margin-left: 12px; font-size: 14px; font-weight: 400; line-height: 40px; } .nav a { display: block; color: inherit; text-decoration: none; } .nav a:hover { border-bottom: 1px solid #DB7676; } .nav button { height: 40px; } .nav button:hover { border-bottom: 1px solid #DB7676; cursor: pointer; } /* background image by Cole Bemis */ .nav .user { padding-left: 20px; background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='18' height='18' viewBox='0 0 24 24' fill='none' stroke='currentColor' stroke-width='2' stroke-linecap='round' stroke-linejoin='round' class='feather feather-user'%3E%3Cpath d='M20 21v-2a4 4 0 0 0-4-4H8a4 4 0 0 0-4 4v2'%3E%3C/path%3E%3Ccircle cx='12' cy='7' r='4'%3E%3C/circle%3E%3C/svg%3E"); background-repeat: no-repeat; background-position: center left; } /* background image by Cole Bemis */ .nav .logout { padding-left: 20px; background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='18' height='18' viewBox='0 0 24 24' fill='none' stroke='currentColor' stroke-width='2' stroke-linecap='round' stroke-linejoin='round' class='feather feather-log-out'%3E%3Cpath d='M9 21H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h4'%3E%3C/path%3E%3Cpolyline points='16 17 21 12 16 7'%3E%3C/polyline%3E%3Cline x1='21' y1='12' x2='9' y2='12'%3E%3C/line%3E%3C/svg%3E%0A"); background-repeat: no-repeat; background-position: center left; } ================================================ FILE: public/css/base.css ================================================ hr { margin: 20px 0; border: 0; border-top: 1px dashed #c5c5c5; border-bottom: 1px dashed #f7f7f7; } .learn a { font-weight: normal; text-decoration: none; color: #b83f45; } .learn a:hover { text-decoration: underline; color: #787e7e; } .learn h3, .learn h4, .learn h5 { margin: 10px 0; font-weight: 500; line-height: 1.2; color: #000; } .learn h3 { font-size: 24px; } .learn h4 { font-size: 18px; } .learn h5 { margin-bottom: 0; font-size: 14px; } .learn ul { padding: 0; margin: 0 0 30px 25px; } .learn li { line-height: 20px; } .learn p { font-size: 15px; font-weight: 300; line-height: 1.3; margin-top: 0; margin-bottom: 0; } #issue-count { display: none; } .quote { border: none; margin: 20px 0 60px 0; } .quote p { font-style: italic; } .quote p:before { content: '“'; font-size: 50px; opacity: .15; position: absolute; top: -20px; left: 3px; } .quote p:after { content: '”'; font-size: 50px; opacity: .15; position: absolute; bottom: -42px; right: 3px; } .quote footer { position: absolute; bottom: -40px; right: 0; } .quote footer img { border-radius: 3px; } .quote footer a { margin-left: 5px; vertical-align: middle; } .speech-bubble { position: relative; padding: 10px; background: rgba(0, 0, 0, .04); border-radius: 5px; } .speech-bubble:after { content: ''; position: absolute; top: 100%; right: 30px; border: 13px solid transparent; border-top-color: rgba(0, 0, 0, .04); } .learn-bar > .learn { position: absolute; width: 272px; top: 8px; left: -300px; padding: 10px; border-radius: 5px; background-color: rgba(255, 255, 255, .6); transition-property: left; transition-duration: 500ms; } @media (min-width: 899px) { .learn-bar { width: auto; padding-left: 300px; } .learn-bar > .learn { left: 8px; } } ================================================ FILE: public/css/home.css ================================================ .todohome { margin: 130px 0 40px 0; position: relative; } .todohome h1 { position: absolute; top: -140px; width: 100%; font-size: 80px; font-weight: 200; text-align: center; color: #b83f45; -webkit-text-rendering: optimizeLegibility; -moz-text-rendering: optimizeLegibility; text-rendering: optimizeLegibility; } .todohome section { padding-top: 1px; text-align: center; } .todohome h2 { padding-bottom: 48px; font-size: 28px; font-weight: 300; } .todohome .button { padding: 13px 45px; font-size: 16px; font-weight: 500; color: white; border-radius: 5px; background: #d83f45; } .todohome a.button { text-decoration: none; } ================================================ FILE: public/css/index.css ================================================ html, body { margin: 0; padding: 0; } button { margin: 0; padding: 0; border: 0; background: none; font-size: 100%; vertical-align: baseline; font-family: inherit; font-weight: inherit; color: inherit; -webkit-appearance: none; appearance: none; -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale; } body { font: 14px 'Helvetica Neue', Helvetica, Arial, sans-serif; line-height: 1.4em; background: #f5f5f5; color: #111111; min-width: 230px; max-width: 550px; margin: 0 auto; -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale; font-weight: 300; } .hidden { display: none; } .todoapp { background: #fff; margin: 130px 0 40px 0; position: relative; box-shadow: 0 2px 4px 0 rgba(0, 0, 0, 0.2), 0 25px 50px 0 rgba(0, 0, 0, 0.1); } .todoapp input::-webkit-input-placeholder { font-style: italic; font-weight: 400; color: rgba(0, 0, 0, 0.4); } .todoapp input::-moz-placeholder { font-style: italic; font-weight: 400; color: rgba(0, 0, 0, 0.4); } .todoapp input::input-placeholder { font-style: italic; font-weight: 400; color: rgba(0, 0, 0, 0.4); } .todoapp h1 { position: absolute; top: -140px; width: 100%; font-size: 80px; font-weight: 200; text-align: center; color: #b83f45; -webkit-text-rendering: optimizeLegibility; -moz-text-rendering: optimizeLegibility; text-rendering: optimizeLegibility; } .new-todo, .edit { position: relative; margin: 0; width: 100%; font-size: 24px; font-family: inherit; font-weight: inherit; line-height: 1.4em; color: inherit; padding: 6px; border: 1px solid #999; box-shadow: inset 0 -1px 5px 0 rgba(0, 0, 0, 0.2); box-sizing: border-box; -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale; } .new-todo { padding: 16px 16px 16px 60px; height: 65px; border: none; background: rgba(0, 0, 0, 0.003); box-shadow: inset 0 -2px 1px rgba(0,0,0,0.03); } .main { position: relative; z-index: 2; border-top: 1px solid #e6e6e6; } .toggle-all { width: 1px; height: 1px; border: none; /* Mobile Safari */ opacity: 0; position: absolute; right: 100%; bottom: 100%; } .toggle-all + label { display: flex; align-items: center; justify-content: center; width: 45px; height: 65px; font-size: 0; position: absolute; top: -65px; left: -0; } .toggle-all + label:before { content: '❯'; display: inline-block; font-size: 22px; color: #949494; padding: 10px 27px 10px 27px; -webkit-transform: rotate(90deg); transform: rotate(90deg); } .toggle-all:checked + label:before { color: #484848; } .todo-list { margin: 0; padding: 0; list-style: none; } .todo-list li { position: relative; font-size: 24px; border-bottom: 1px solid #ededed; } .todo-list li:last-child { border-bottom: none; } .todo-list li.editing { border-bottom: none; padding: 0; } .todo-list li.editing .edit { display: block; width: calc(100% - 43px); padding: 12px 16px; margin: 0 0 0 43px; } .todo-list li.editing .view { display: none; } .todo-list li .toggle { text-align: center; width: 40px; /* auto, since non-WebKit browsers doesn't support input styling */ height: auto; position: absolute; top: 0; bottom: 0; margin: auto 0; border: none; /* Mobile Safari */ -webkit-appearance: none; appearance: none; } .todo-list li .toggle { opacity: 0; } .todo-list li .toggle + label { /* Firefox requires `#` to be escaped - https://bugzilla.mozilla.org/show_bug.cgi?id=922433 IE and Edge requires *everything* to be escaped to render, so we do that instead of just the `#` - https://developer.microsoft.com/en-us/microsoft-edge/platform/issues/7157459/ */ background-image: url('data:image/svg+xml;utf8,%3Csvg%20xmlns%3D%22http%3A//www.w3.org/2000/svg%22%20width%3D%2240%22%20height%3D%2240%22%20viewBox%3D%22-10%20-18%20100%20135%22%3E%3Ccircle%20cx%3D%2250%22%20cy%3D%2250%22%20r%3D%2250%22%20fill%3D%22none%22%20stroke%3D%22%23949494%22%20stroke-width%3D%223%22/%3E%3C/svg%3E'); background-repeat: no-repeat; background-position: center left; } .todo-list li .toggle:checked + label { background-image: url('data:image/svg+xml;utf8,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20width%3D%2240%22%20height%3D%2240%22%20viewBox%3D%22-10%20-18%20100%20135%22%3E%3Ccircle%20cx%3D%2250%22%20cy%3D%2250%22%20r%3D%2250%22%20fill%3D%22none%22%20stroke%3D%22%2359A193%22%20stroke-width%3D%223%22%2F%3E%3Cpath%20fill%3D%22%233EA390%22%20d%3D%22M72%2025L42%2071%2027%2056l-4%204%2020%2020%2034-52z%22%2F%3E%3C%2Fsvg%3E'); } .todo-list li label { word-break: break-all; padding: 15px 15px 15px 60px; display: block; line-height: 1.2; transition: color 0.4s; font-weight: 400; color: #484848; } .todo-list li.completed label { color: #949494; text-decoration: line-through; } .todo-list li .destroy { display: none; position: absolute; top: 0; right: 10px; bottom: 0; width: 40px; height: 40px; margin: auto 0; font-size: 30px; color: #949494; transition: color 0.2s ease-out; } .todo-list li .destroy:hover, .todo-list li .destroy:focus { color: #C18585; } .todo-list li .destroy:after { content: '×'; display: block; height: 100%; line-height: 1.1; } .todo-list li:hover .destroy { display: block; } .todo-list li .edit { display: none; } .todo-list li.editing:last-child { margin-bottom: -1px; } .footer { padding: 10px 15px; height: 20px; text-align: center; font-size: 15px; border-top: 1px solid #e6e6e6; } .footer:before { content: ''; position: absolute; right: 0; bottom: 0; left: 0; height: 50px; overflow: hidden; box-shadow: 0 1px 1px rgba(0, 0, 0, 0.2), 0 8px 0 -3px #f6f6f6, 0 9px 1px -3px rgba(0, 0, 0, 0.2), 0 16px 0 -6px #f6f6f6, 0 17px 2px -6px rgba(0, 0, 0, 0.2); } .todo-count { float: left; text-align: left; } .todo-count strong { font-weight: 300; } .filters { margin: 0; padding: 0; list-style: none; position: absolute; right: 0; left: 0; } .filters li { display: inline; } .filters li a { color: inherit; margin: 3px; padding: 3px 7px; text-decoration: none; border: 1px solid transparent; border-radius: 3px; } .filters li a:hover { border-color: #DB7676; } .filters li a.selected { border-color: #CE4646; } .clear-completed, html .clear-completed:active { float: right; position: relative; line-height: 19px; text-decoration: none; cursor: pointer; } .clear-completed:hover { text-decoration: underline; } .info { margin: 65px auto 0; color: #4d4d4d; font-size: 11px; text-shadow: 0 1px 0 rgba(255, 255, 255, 0.5); text-align: center; } .info p { line-height: 1; } .info a { color: inherit; text-decoration: none; font-weight: 400; } .info a:hover { text-decoration: underline; } /* Hack to remove background from Mobile Safari. Can't use it globally since it destroys checkboxes in Firefox */ @media screen and (-webkit-min-device-pixel-ratio:0) { .toggle-all, .todo-list li .toggle { background: none; } .todo-list li .toggle { height: 40px; } } @media (max-width: 430px) { .footer { height: 50px; } .filters { bottom: 10px; } } :focus, .toggle:focus + label, .toggle-all:focus + label { box-shadow: 0 0 2px 2px #CF7D7D; outline: 0; } ================================================ FILE: public/css/login.css ================================================ .prompt { max-width: 400px; margin: 50px auto; padding: 25px; background: #fff; border: 1px solid #e6e6e6; border-radius: 8px; } button { display: block; padding: 10px; width: 100%; border-radius: 3px; background: #d83f45; font-size: 14px; font-weight: 700; color: white; cursor: pointer; } a.button { box-sizing: border-box; display: block; padding: 10px; width: 100%; border-radius: 3px; background: #000; font-size: 14px; font-weight: 700; text-align: center; text-decoration: none; color: white; } a.facebook { background: #4267b2; } button:hover { background-color: #c83f45; } h1 { margin: 0 0 20px 0; padding: 0 0 5px 0; font-size: 24px; font-weight: 500; } h3 { margin-top: 0; font-size: 24px; font-weight: 300; text-align: center; color: #b83f45; } form section { margin: 0 0 20px 0; position: relative; /* for password toggle positioning */ } label { display: block; margin: 0 0 3px 0; font-size: 14px; font-weight: 500; } input { box-sizing: border-box; width: 100%; padding: 10px; font-size: 14px; border: 1px solid #d9d9d9; border-radius: 5px; } input[type=email]:not(:focus):invalid, input[type=password]:not(:focus):invalid { color: red; outline-color: red; } hr { border-top: 1px solid #d9d9d9; border-bottom: none; } p.help { text-align: center; font-weight: 400; } /* background image by Cole Bemis */ .messages p { font-size: 14px; font-weight: 400; line-height: 1.3; color: #d83f45; padding-left: 20px; background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16' viewBox='0 0 24 24' fill='none' stroke='%23d83f45' stroke-width='2' stroke-linecap='round' stroke-linejoin='round' class='feather feather-alert-circle'%3E%3Ccircle cx='12' cy='12' r='10'%3E%3C/circle%3E%3Cline x1='12' y1='8' x2='12' y2='12'%3E%3C/line%3E%3Cline x1='12' y1='16' x2='12.01' y2='16'%3E%3C/line%3E%3C/svg%3E"); background-repeat: no-repeat; background-position: center left; } ================================================ FILE: routes/auth.js ================================================ var express = require('express'); var passport = require('passport'); var FacebookStrategy = require('passport-facebook'); var db = require('../db'); passport.use(new FacebookStrategy({ clientID: process.env['FACEBOOK_CLIENT_ID'], clientSecret: process.env['FACEBOOK_CLIENT_SECRET'], callbackURL: '/oauth2/redirect/facebook', state: true }, function verify(accessToken, refreshToken, profile, cb) { db.get('SELECT * FROM federated_credentials WHERE provider = ? AND subject = ?', [ 'https://www.facebook.com', profile.id ], function(err, row) { if (err) { return cb(err); } if (!row) { db.run('INSERT INTO users (name) VALUES (?)', [ profile.displayName ], function(err) { if (err) { return cb(err); } var id = this.lastID; db.run('INSERT INTO federated_credentials (user_id, provider, subject) VALUES (?, ?, ?)', [ id, 'https://www.facebook.com', profile.id ], function(err) { if (err) { return cb(err); } var user = { id: id, name: profile.displayName }; return cb(null, user); }); }); } else { db.get('SELECT * FROM users WHERE id = ?', [ row.user_id ], function(err, row) { if (err) { return cb(err); } if (!row) { return cb(null, false); } return cb(null, row); }); } }); })); passport.serializeUser(function(user, cb) { process.nextTick(function() { cb(null, { id: user.id, username: user.username, name: user.name }); }); }); passport.deserializeUser(function(user, cb) { process.nextTick(function() { return cb(null, user); }); }); var router = express.Router(); router.get('/login', function(req, res, next) { res.render('login'); }); router.get('/login/federated/facebook', passport.authenticate('facebook')); router.get('/oauth2/redirect/facebook', passport.authenticate('facebook', { successReturnToOrRedirect: '/', failureRedirect: '/login' })); router.post('/logout', function(req, res, next) { req.logout(function(err) { if (err) { return next(err); } res.redirect('/'); }); }); module.exports = router; ================================================ FILE: routes/index.js ================================================ var express = require('express'); var ensureLogIn = require('connect-ensure-login').ensureLoggedIn; var db = require('../db'); var ensureLoggedIn = ensureLogIn(); function fetchTodos(req, res, next) { db.all('SELECT * FROM todos WHERE owner_id = ?', [ req.user.id ], function(err, rows) { if (err) { return next(err); } var todos = rows.map(function(row) { return { id: row.id, title: row.title, completed: row.completed == 1 ? true : false, url: '/' + row.id } }); res.locals.todos = todos; res.locals.activeCount = todos.filter(function(todo) { return !todo.completed; }).length; res.locals.completedCount = todos.length - res.locals.activeCount; next(); }); } var router = express.Router(); /* GET home page. */ router.get('/', function(req, res, next) { if (!req.user) { return res.render('home'); } next(); }, fetchTodos, function(req, res, next) { res.locals.filter = null; res.render('index', { user: req.user }); }); router.get('/active', ensureLoggedIn, fetchTodos, function(req, res, next) { res.locals.todos = res.locals.todos.filter(function(todo) { return !todo.completed; }); res.locals.filter = 'active'; res.render('index', { user: req.user }); }); router.get('/completed', ensureLoggedIn, fetchTodos, function(req, res, next) { res.locals.todos = res.locals.todos.filter(function(todo) { return todo.completed; }); res.locals.filter = 'completed'; res.render('index', { user: req.user }); }); router.post('/', ensureLoggedIn, function(req, res, next) { req.body.title = req.body.title.trim(); next(); }, function(req, res, next) { if (req.body.title !== '') { return next(); } return res.redirect('/' + (req.body.filter || '')); }, function(req, res, next) { db.run('INSERT INTO todos (owner_id, title, completed) VALUES (?, ?, ?)', [ req.user.id, req.body.title, req.body.completed == true ? 1 : null ], function(err) { if (err) { return next(err); } return res.redirect('/' + (req.body.filter || '')); }); }); router.post('/:id(\\d+)', ensureLoggedIn, function(req, res, next) { req.body.title = req.body.title.trim(); next(); }, function(req, res, next) { if (req.body.title !== '') { return next(); } db.run('DELETE FROM todos WHERE id = ? AND owner_id = ?', [ req.params.id, req.user.id ], function(err) { if (err) { return next(err); } return res.redirect('/' + (req.body.filter || '')); }); }, function(req, res, next) { db.run('UPDATE todos SET title = ?, completed = ? WHERE id = ? AND owner_id = ?', [ req.body.title, req.body.completed !== undefined ? 1 : null, req.params.id, req.user.id ], function(err) { if (err) { return next(err); } return res.redirect('/' + (req.body.filter || '')); }); }); router.post('/:id(\\d+)/delete', ensureLoggedIn, function(req, res, next) { db.run('DELETE FROM todos WHERE id = ? AND owner_id = ?', [ req.params.id, req.user.id ], function(err) { if (err) { return next(err); } return res.redirect('/' + (req.body.filter || '')); }); }); router.post('/toggle-all', ensureLoggedIn, function(req, res, next) { db.run('UPDATE todos SET completed = ? WHERE owner_id = ?', [ req.body.completed !== undefined ? 1 : null, req.user.id ], function(err) { if (err) { return next(err); } return res.redirect('/' + (req.body.filter || '')); }); }); router.post('/clear-completed', ensureLoggedIn, function(req, res, next) { db.run('DELETE FROM todos WHERE owner_id = ? AND completed = ?', [ req.user.id, 1 ], function(err) { if (err) { return next(err); } return res.redirect('/' + (req.body.filter || '')); }); }); module.exports = router; ================================================ FILE: views/error.ejs ================================================

<%= message %>

<%= error.status %>

<%= error.stack %>
================================================ FILE: views/home.ejs ================================================ Express • TodoMVC

todos

todos helps you get things done

Sign in
================================================ FILE: views/index.ejs ================================================ Express • TodoMVC

todos

<% if (filter) { %> <% } %>
<% if (activeCount + completedCount > 0) { %>
onchange="this.form.submit();">
    <% todos.forEach(function(todo) { %>
  • >
    onchange="this.form.submit();">
    <% if (filter) { %> <% } %>
    <% if (filter) { %> <% } %>
  • <% }); %>
<% } %> <% if (activeCount + completedCount > 0) { %> <% } %>
================================================ FILE: views/login.ejs ================================================ Express • TodoMVC

todos

Sign in

<% if (hasMessages) { %>
<% messages.forEach(function(message) { %>

<%= message %>

<% }); %>
<% } %>