ff-stream-web

git clone git://archive.git.mtrnord.blog/MTRNord/ff-stream-web.git
Log | Files | Refs | README | LICENSE

commit 689a81f6362b52f9cba417a4cd147a915565534e
parent a54cac9299d27fe714df1987e7511e450f4093cb
Author: Konstantin Tarkus <hello@tarkus.me>
Date:   Fri, 26 Feb 2016 01:51:07 +0300

Add Passport.js authentication strategy sample

Diffstat:
Mpackage.json | 7+++++++
Msrc/config.js | 8++++++--
Asrc/core/db.js | 81+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/core/passport.js | 111+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Msrc/server.js | 35++++++++++++++++++++++++++++++++++-
5 files changed, 239 insertions(+), 3 deletions(-)

diff --git a/package.json b/package.json @@ -8,17 +8,24 @@ "babel-polyfill": "^6.5.0", "babel-runtime": "^6.5.0", "bluebird": "3.3.1", + "body-parser": "1.15.0", "classnames": "2.2.3", + "cookie-parser": "1.4.1", "eventemitter3": "1.1.1", "express": "4.13.4", + "express-jwt": "3.3.0", "fastclick": "1.0.6", "fbjs": "0.7.2", "front-matter": "2.0.6", "history": "2.0.0", "isomorphic-style-loader": "0.0.10", "jade": "1.11.0", + "jsonwebtoken": "5.7.0", "node-fetch": "1.3.3", "normalize.css": "3.0.3", + "passport": "0.3.2", + "passport-facebook": "2.1.0", + "pg": "4.4.6", "react": "0.14.7", "react-dom": "0.14.7", "react-routing": "0.0.7", diff --git a/src/config.js b/src/config.js @@ -13,6 +13,8 @@ export const port = process.env.PORT || 5000; export const host = process.env.WEBSITE_HOSTNAME || `localhost:${port}`; +export const databaseUrl = process.env.DATABASE_URL || 'postgresql://demo:Lqk62xg6TBm5UhfR@demo.ctbl5itzitm4.us-east-1.rds.amazonaws.com:5432/membership01'; + export const analytics = { // https://analytics.google.com/ @@ -22,10 +24,12 @@ export const analytics = { export const auth = { + jwt: { secret: 'React Starter Kit' }, + // https://developers.facebook.com/ facebook: { - id: process.env.FACEBOOK_ID || '183246425378777', - secret: process.env.FACEBOOK_SECRET || 'cb2b201f0249d15454221cbf00d6ff99', + id: process.env.FACEBOOK_APP_ID || '186244551745631', + secret: process.env.FACEBOOK_APP_SECRET || 'a970ae3240ab4b9b8aae0f9f0661c6fc', }, // https://cloud.google.com/console/project diff --git a/src/core/db.js b/src/core/db.js @@ -0,0 +1,81 @@ +/** + * React Starter Kit (https://www.reactstarterkit.com/) + * + * Copyright © 2014-2016 Kriasoft, LLC. All rights reserved. + * + * This source code is licensed under the MIT license found in the + * LICENSE.txt file in the root directory of this source tree. + */ + +import db from 'pg'; +import Promise from 'bluebird'; +import { databaseUrl } from '../config'; + +// TODO: Customize database connection settings +/* jscs:disable requireCamelCaseOrUpperCaseIdentifiers */ +db.defaults.ssl = true; +db.defaults.poolSize = 2; +db.defaults.application_name = 'RSK'; +/* jscs:enable requireCamelCaseOrUpperCaseIdentifiers */ + +/** + * Promise-based wrapper for pg.Client + * https://github.com/brianc/node-postgres/wiki/Client + */ +function AsyncClient(client) { + this.client = client; + this.query = this.query.bind(this); + this.end = this.end.bind(this); +} + +AsyncClient.prototype.query = function query(sql, ...args) { + return new Promise((resolve, reject) => { + if (args.length) { + this.client.query(sql, args, (err, result) => { + if (err) { + reject(err); + } else { + resolve(result); + } + }); + } else { + this.client.query(sql, (err, result) => { + if (err) { + reject(err); + } else { + resolve(result); + } + }); + } + }); +}; + +AsyncClient.prototype.end = function end() { + this.client.end(); +}; + +/** + * Promise-based wrapper for pg.connect() + * https://github.com/brianc/node-postgres/wiki/pg + */ +db.connect = (connect => callback => new Promise((resolve, reject) => { + connect.call(db, databaseUrl, (err, client, done) => { + if (err) { + if (client) { + done(client); + } + + reject(err); + } else { + callback(new AsyncClient(client)).then(() => { + done(); + resolve(); + }).catch(error => { + done(client); + reject(error); + }); + } + }); +}))(db.connect); + +export default db; diff --git a/src/core/passport.js b/src/core/passport.js @@ -0,0 +1,111 @@ +/** + * React Starter Kit (https://www.reactstarterkit.com/) + * + * Copyright © 2014-2016 Kriasoft, LLC. All rights reserved. + * + * This source code is licensed under the MIT license found in the + * LICENSE.txt file in the root directory of this source tree. + */ + +/** + * Passport.js reference implementation. + * The database schema used in this sample is available at + * https://github.com/membership/membership.db/tree/master/postgres + */ + +import passport from 'passport'; +import { Strategy as FacebookStrategy } from 'passport-facebook'; +import db from './db'; +import { auth as config } from '../config'; + +/** + * Sign in with Facebook. + */ +passport.use(new FacebookStrategy({ + clientID: config.facebook.id, + clientSecret: config.facebook.secret, + callbackURL: `/login/facebook/return`, + profileFields: ['name', 'email', 'link', 'locale', 'timezone'], + passReqToCallback: true, +}, (req, accessToken, refreshToken, profile, done) => { + const loginName = 'facebook'; + db.connect(async ({ query }) => { + if (req.user) { + let result = await query( + 'SELECT 1 FROM user_login WHERE name = $1 AND key = $2', + loginName, profile.id + ); + if (result.rowCount) { + // There is already a Facebook account that belongs to you. + // Sign in with that account or delete it, then link it with your current account. + done(); + } else { + await query(` + INSERT INTO user_account (id, email) SELECT $1, $2::character + WHERE NOT EXISTS (SELECT 1 FROM user_account WHERE id = $1);`, + req.user.id, profile._json.email); + await query(` + INSERT INTO user_login (user_id, name, key) VALUES ($1, 'facebook', $2);`, + req.user.id, profile.id); + await query(` + INSERT INTO user_claim (user_id, type, value) VALUES + ($1, 'urn:facebook:access_token', $3);`, + req.user.id, profile.id); + await query(` + INSERT INTO user_profile (user_id) SELECT $1 + WHERE NOT EXISTS (SELECT 1 FROM user_profile WHERE user_id = $1);`, + req.user.id); + await query(` + UPDATE user_profile SET + display_name = COALESCE(NULLIF(display_name, ''), $2), + gender = COALESCE(NULLIF(gender, ''), $3), + picture = COALESCE(NULLIF(picture, ''), $4), + WHERE user_id = $1;`, + req.user.id, profile.displayName, profile._json.gender, + `https://graph.facebook.com/${profile.id}/picture?type=large`); + result = await query(` + SELECT id, email FROM user_account WHERE id = $1;`, + req.user.id); + done(null, result.rows[0]); + } + } else { + let result = await query(` + SELECT u.id, u.email FROM user_account AS u + LEFT JOIN user_login AS l ON l.user_id = u.id + WHERE l.name = $1 AND l.key = $2`, loginName, profile.id); + if (result.rowCount) { + done(null, result.rows[0]); + } else { + result = await query(`SELECT 1 FROM user_account WHERE email = $1`, profile._json.email); + if (result.rowCount) { + // There is already an account using this email address. Sign in to + // that account and link it with Facebook manually from Account Settings. + done(null); + } else { + result = await query(` + INSERT INTO user_account (email) VALUES ($1) RETURNING (id)`, + profile._json.email + ); + const userId = result.rows[0].id; + await query(` + INSERT INTO user_login (user_id, name, key) VALUES ($1, 'facebook', $2)`, + userId, profile.id); + await query(` + INSERT INTO user_claim (user_id, type, value) VALUES + ($1, 'urn:facebook:access_token', $2);`, + userId, accessToken); + await query(` + INSERT INTO user_profile (user_id, display_name, gender, picture) + VALUES ($1, $2, $3, $4);`, + userId, profile.displayName, profile._json.gender, + `https://graph.facebook.com/${profile.id}/picture?type=large` + ); + result = await query(`SELECT id, email FROM user_account WHERE id = $1;`, userId); + done(null, result.rows[0]); + } + } + } + }).catch(done); +})); + +export default passport; diff --git a/src/server.js b/src/server.js @@ -10,12 +10,17 @@ import 'babel-polyfill'; import path from 'path'; import express from 'express'; +import cookieParser from 'cookie-parser'; +import bodyParser from 'body-parser'; +import expressJwt from 'express-jwt'; +import jwt from 'jsonwebtoken'; import React from 'react'; import ReactDOM from 'react-dom/server'; +import passport from './core/passport'; import Router from './routes'; import Html from './components/Html'; import assets from './assets'; -import { port } from './config'; +import { port, auth } from './config'; const server = global.server = express(); @@ -30,6 +35,34 @@ global.navigator.userAgent = global.navigator.userAgent || 'all'; // Register Node.js middleware // ----------------------------------------------------------------------------- server.use(express.static(path.join(__dirname, 'public'))); +server.use(cookieParser()); +server.use(bodyParser.urlencoded({ extended: true })); +server.use(bodyParser.json()); + +// +// Authentication +// ----------------------------------------------------------------------------- +server.use(expressJwt({ + secret: auth.jwt.secret, + credentialsRequired: false, + /* jscs:disable requireCamelCaseOrUpperCaseIdentifiers */ + getToken: req => req.cookies.id_token, + /* jscs:enable requireCamelCaseOrUpperCaseIdentifiers */ +})); +server.use(passport.initialize()); + +server.get('/login/facebook', + passport.authenticate('facebook', { scope: ['email', 'user_location'], session: false }) +); +server.get('/login/facebook/return', + passport.authenticate('facebook', { failureRedirect: '/login', session: false }), + (req, res) => { + const expiresIn = 60 * 60 * 24 * 180; // 180 days + const token = jwt.sign(req.user, auth.jwt.secret, { expiresIn }); + res.cookie('id_token', token, { maxAge: 1000 * expiresIn, httpOnly: true }); + res.redirect('/'); + } +); // // Register API middleware