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 <http://unlicense.org/>
================================================
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 <https://feathericons.com> */
.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 <https://feathericons.com> */
.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 <https://feathericons.com> */
.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
================================================
<h1><%= message %></h1>
<h2><%= error.status %></h2>
<pre><%= error.stack %></pre>
================================================
FILE: views/home.ejs
================================================
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Express • TodoMVC</title>
<link rel="stylesheet" href="/css/base.css">
<link rel="stylesheet" href="/css/index.css">
<link rel="stylesheet" href="/css/home.css">
</head>
<body>
<section class="todohome">
<header>
<h1>todos</h1>
</header>
<section>
<h2>todos helps you get things done</h2>
<a class="button" href="/login">Sign in</a>
</section>
</section>
<footer class="info">
<p>Created by <a href="https://www.jaredhanson.me">Jared Hanson</a></p>
<p>Part of <a href="https://todomvc.com">TodoMVC</a></p>
<p>Authentication powered by <a href="https://www.passportjs.org">Passport</a></p>
</footer>
</body>
</html>
================================================
FILE: views/index.ejs
================================================
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Express • TodoMVC</title>
<link rel="stylesheet" href="/css/base.css">
<link rel="stylesheet" href="/css/index.css">
<link rel="stylesheet" href="/css/app.css">
</head>
<body>
<section class="todoapp">
<nav class="nav">
<ul>
<li class="user"><%= user.name || user.username %></li>
<li>
<form action="/logout" method="post">
<button class="logout" type="submit">Sign out</button>
<input type="hidden" name="_csrf" value="<%= csrfToken %>">
</form>
</li>
</ul>
</nav>
<header class="header">
<h1>todos</h1>
<form action="/" method="post">
<input class="new-todo" name="title" placeholder="What needs to be done?" autofocus>
<% if (filter) { %>
<input type="hidden" name="filter" value="<%= filter %>"/>
<% } %>
<input type="hidden" name="_csrf" value="<%= csrfToken %>">
</form>
</header>
<% if (activeCount + completedCount > 0) { %>
<section class="main">
<form action="/toggle-all" method="post">
<input id="toggle-all" class="toggle-all" type="checkbox" name="completed" <%- activeCount == 0 ? 'checked' : '' %> onchange="this.form.submit();">
<label for="toggle-all">Mark all as complete</label>
<input type="hidden" name="_csrf" value="<%= csrfToken %>">
</form>
<ul class="todo-list">
<% todos.forEach(function(todo) { %>
<li <%- todo.completed ? 'class="completed"' : '' %>>
<form action="<%= todo.url %>" method="post">
<div class="view">
<input class="toggle" type="checkbox" name="completed" <%- todo.completed ? 'checked' : '' %> onchange="this.form.submit();">
<label ondblclick="this.closest('li').className = this.closest('li').className + ' editing'; this.closest('li').querySelector('input.edit').focus(); this.closest('li').querySelector('input.edit').value = ''; this.closest('li').querySelector('input.edit').value = '<%= todo.title %>';"><%= todo.title %></label>
<button class="destroy" form="delete-<%= todo.id %>"></button>
</div>
<input class="edit" name="title" value="<%= todo.title %>" onkeyup="if (event.keyCode == 27) { this.setAttribute('data-esc', ''); this.closest('li').className = this.closest('li').className.replace('editing', ''); }" onblur="if (this.getAttribute('data-esc') !== null) { return this.removeAttribute('data-esc'); } this.form.submit();">
<% if (filter) { %>
<input type="hidden" name="filter" value="<%= filter %>"/>
<% } %>
<input type="hidden" name="_csrf" value="<%= csrfToken %>">
</form>
<form id="delete-<%= todo.id %>" action="<%= todo.url %>/delete" method="post">
<% if (filter) { %>
<input type="hidden" name="filter" value="<%= filter %>"/>
<% } %>
<input type="hidden" name="_csrf" value="<%= csrfToken %>">
</form>
</li>
<% }); %>
</ul>
</section>
<% } %>
<% if (activeCount + completedCount > 0) { %>
<footer class="footer">
<span class="todo-count"><strong><%= activeCount %></strong> <%= pluralize('item', activeCount) %> left</span>
<ul class="filters">
<li>
<a <%- !filter ? 'class="selected"' : '' %> href="/">All</a>
</li>
<li>
<a <%- filter == 'active' ? 'class="selected"' : '' %> href="/active">Active</a>
</li>
<li>
<a <%- filter == 'completed' ? 'class="selected"' : '' %> href="/completed">Completed</a>
</li>
</ul>
<% if (completedCount > 0) { %>
<form action="/clear-completed" method="post">
<button class="clear-completed">Clear completed</button>
<% if (filter) { %>
<input type="hidden" name="filter" value="<%= filter %>"/>
<% } %>
<input type="hidden" name="_csrf" value="<%= csrfToken %>">
</form>
<% } %>
</footer>
<% } %>
</section>
<footer class="info">
<p>Double-click to edit a todo</p>
<p>Created by <a href="https://www.jaredhanson.me">Jared Hanson</a></p>
<p>Part of <a href="https://todomvc.com">TodoMVC</a></p>
<p>Authentication powered by <a href="https://www.passportjs.org">Passport</a></p>
</footer>
</body>
</html>
================================================
FILE: views/login.ejs
================================================
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Express • TodoMVC</title>
<link rel="stylesheet" href="/css/base.css">
<link rel="stylesheet" href="/css/index.css">
<link rel="stylesheet" href="/css/login.css">
</head>
<body>
<section class="prompt">
<h3>todos</h3>
<h1>Sign in</h1>
<% if (hasMessages) { %>
<section class="messages">
<% messages.forEach(function(message) { %>
<p><%= message %></p>
<% }); %>
</section>
<% } %>
<a class="button facebook" href="/login/federated/facebook">Sign in with Facebook</a>
</section>
<footer class="info">
<p>Created by <a href="https://www.jaredhanson.me">Jared Hanson</a></p>
<p>Part of <a href="https://todomvc.com">TodoMVC</a></p>
<p>Authentication powered by <a href="https://www.passportjs.org">Passport</a></p>
</footer>
</body>
</html>
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
SYMBOL INDEX (1 symbols across 1 files)
FILE: routes/index.js
function fetchTodos (line 7) | function fetchTodos(req, res, next) {
Condensed preview — 19 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (39K chars).
[
{
"path": ".github/FUNDING.yml",
"chars": 20,
"preview": "github: jaredhanson\n"
},
{
"path": ".gitignore",
"chars": 71,
"preview": ".env\nvar\n\n# Node.js\nnode_modules/\nnpm-debug.log*\n\n# Mac OS X\n.DS_Store\n"
},
{
"path": "LICENSE",
"chars": 1210,
"preview": "This is free and unencumbered software released into the public domain.\n\nAnyone is free to copy, modify, publish, use, c"
},
{
"path": "README.md",
"chars": 3119,
"preview": "This example demonstrates how to use [Express](https://expressjs.com) 4.x and\n[Passport](https://www.passportjs.org) to "
},
{
"path": "app.js",
"chars": 1984,
"preview": "require('dotenv').config();\n\nvar createError = require('http-errors');\nvar express = require('express');\nvar path = requ"
},
{
"path": "bin/www",
"chars": 1591,
"preview": "#!/usr/bin/env node\n\n/**\n * Module dependencies.\n */\n\nvar app = require('../app');\nvar debug = require('debug')('todos:s"
},
{
"path": "db.js",
"chars": 820,
"preview": "var sqlite3 = require('sqlite3');\nvar mkdirp = require('mkdirp');\n\nmkdirp.sync('var/db');\n\nvar db = new sqlite3.Database"
},
{
"path": "package.json",
"chars": 1272,
"preview": "{\n \"name\": \"todos-express-facebook\",\n \"version\": \"0.0.0\",\n \"private\": true,\n \"description\": \"Todo app using Express "
},
{
"path": "public/css/app.css",
"chars": 1649,
"preview": ".nav {\n\tposition: absolute;\n\ttop: -130px;\n\tright: 0;\n}\n\n.nav ul {\n\tmargin: 0;\n\tlist-style: none;\n\ttext-align: center;\n}\n"
},
{
"path": "public/css/base.css",
"chars": 1814,
"preview": "hr {\n\tmargin: 20px 0;\n\tborder: 0;\n\tborder-top: 1px dashed #c5c5c5;\n\tborder-bottom: 1px dashed #f7f7f7;\n}\n\n.learn a {\n\tfo"
},
{
"path": "public/css/home.css",
"chars": 654,
"preview": ".todohome {\n\tmargin: 130px 0 40px 0;\n\tposition: relative;\n}\n\n.todohome h1 {\n\tposition: absolute;\n\ttop: -140px;\n\twidth: 1"
},
{
"path": "public/css/index.css",
"chars": 7251,
"preview": "html,\nbody {\n\tmargin: 0;\n\tpadding: 0;\n}\n\nbutton {\n\tmargin: 0;\n\tpadding: 0;\n\tborder: 0;\n\tbackground: none;\n\tfont-size: 10"
},
{
"path": "public/css/login.css",
"chars": 2022,
"preview": ".prompt {\n\tmax-width: 400px;\n\tmargin: 50px auto;\n\tpadding: 25px;\n\tbackground: #fff;\n\tborder: 1px solid #e6e6e6;\n\tborder-"
},
{
"path": "routes/auth.js",
"chars": 2198,
"preview": "var express = require('express');\nvar passport = require('passport');\nvar FacebookStrategy = require('passport-facebook'"
},
{
"path": "routes/index.js",
"chars": 3768,
"preview": "var express = require('express');\nvar ensureLogIn = require('connect-ensure-login').ensureLoggedIn;\nvar db = require('.."
},
{
"path": "views/error.ejs",
"chars": 83,
"preview": "<h1><%= message %></h1>\n<h2><%= error.status %></h2>\n<pre><%= error.stack %></pre>\n"
},
{
"path": "views/home.ejs",
"chars": 811,
"preview": "<!doctype html>\n<html lang=\"en\">\n\t<head>\n\t\t<meta charset=\"utf-8\">\n\t\t<meta name=\"viewport\" content=\"width=device-width, i"
},
{
"path": "views/index.ejs",
"chars": 4290,
"preview": "<!doctype html>\n<html lang=\"en\">\n\t<head>\n\t\t<meta charset=\"utf-8\">\n\t\t<meta name=\"viewport\" content=\"width=device-width, i"
},
{
"path": "views/login.ejs",
"chars": 943,
"preview": "<!doctype html>\n<html lang=\"en\">\n\t<head>\n\t\t<meta charset=\"utf-8\">\n\t\t<meta name=\"viewport\" content=\"width=device-width, i"
}
]
About this extraction
This page contains the full source code of the passport/express-4.x-facebook-example GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 19 files (34.7 KB), approximately 11.0k tokens, and a symbol index with 1 extracted functions, classes, methods, constants, and types. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.
Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.