Added facebook login support + redis cache

This commit is contained in:
Wyatt Johnson
2016-11-11 12:53:51 -07:00
parent 6c13e1c1c9
commit f6184fddb0
7 changed files with 185 additions and 24 deletions
+6 -3
View File
@@ -19,9 +19,12 @@ Runs Talk.
The Talk application requires specific configuration options to be available
inside the environment in order to run, those variables are listed here:
- `TALK_SESSION_SECRET` (*required*) -
- `TALK_FACEBOOK_APP_ID` (*required*) -
- `TALK_FACEBOOK_APP_SECRET` (*required*) -
- `TALK_SESSION_SECRET` (*required*) - a random string which will be used to
secure cookies
- `TALK_FACEBOOK_APP_ID` (*required*) - the Facebook app id for your Facebook
Login enabled app.
- `TALK_FACEBOOK_APP_SECRET` (*required*) - the Facebook app secret for your
Facebook Login enabled app.
- `TALK_ROOT_URL` (*required*) - Root url of the installed application externally available in the format: `<scheme>://<host>` without the path.
### Running with Docker
+1
View File
@@ -38,6 +38,7 @@ const session_opts = {
rolling: true,
saveUninitialized: false,
resave: false,
name: 'talk.sid',
cookie: {
secure: false,
maxAge: 18000000, // 30 minutes for expiry.
+97
View File
@@ -0,0 +1,97 @@
const redis = require('./redis');
const cache = module.exports = {};
/**
* This collects a key that may either be an array or a string and creates a
* unified key out of it.
* @param {Mixed} key Either an array of items composing a key or a string
* @return {String} A string that represents a key
*/
const keyfunc = (key) => {
if (Array.isArray(key)) {
return `cache[${key.join(':')}]`;
}
return `cache[${key}]`;
};
/**
* This wraps a complicated function with a cache, in the event that the item is
* not inside the cache, it will perform the work to get it and then set it
* followed by returning the value.
* @param {Mixed} key Either an array of items or string represening this
* work
* @param {Integer} expiry Time in seconds for the cache entry to live for
* @param {Function} work A function that returns a promise that can be
* resolved as the value to cache.
* @return {Promise} Resolves to the value either retrieved from cache
*/
cache.wrap = (key, expiry, work) => {
return cache
.get(key)
.then((value) => {
if (value !== null) {
return value;
}
return work()
.then((value) => {
return cache
.set(key, value, expiry)
.then(() => value);
});
});
};
/**
* This returns a promise that returns a promise that resolves with the value
* from the cache or null if it does not exist in the cache.
* @param {Mixed} key Either an array of items composing a key or a string
* @return {Promise}
*/
cache.get = (key) => new Promise((resolve, reject) => {
redis.get(keyfunc(key), (err, reply) => {
if (err) {
return reject(err);
}
if (reply !== null) {
let value;
try {
// Parse the stored cache value from JSON.
value = JSON.parse(reply);
} catch (e) {
return reject(e);
}
return resolve(value);
}
resolve(null);
});
});
/**
* This sets a value on the key with the expiry and then resolves once it is
* done.
* @param {Mixed} key Either an array of items composing a key or a string
* @param {Mixed} value Object to be serialized and set to the cache
* @param {Integer} expiry Time in seconds for the cache entry to live for
* @return {Promise}
*/
cache.set = (key, value, expiry) => new Promise((resolve, reject) => {
// Serialize the value as JSON.
let reply = JSON.stringify(value);
redis.set(keyfunc(key), reply, 'EX', expiry, (err) => {
if (err) {
return reject(err);
}
return resolve();
});
});
+1 -1
View File
@@ -65,7 +65,7 @@ if (process.env.TALK_FACEBOOK_APP_ID && process.env.TALK_FACEBOOK_APP_SECRET &&
passport.use(new FacebookStrategy({
clientID: process.env.TALK_FACEBOOK_APP_ID,
clientSecret: process.env.TALK_FACEBOOK_APP_SECRET,
callbackURL: `${process.env.TALK_ROOT_URL}/connect/facebook/callback`
callbackURL: `${process.env.TALK_ROOT_URL}/api/v1/auth/facebook/callback`
}, (accessToken, refreshToken, profile, done) => {
User
.findOrCreateExternalUser(profile)
+70 -19
View File
@@ -4,40 +4,91 @@ const authorization = require('../../../middleware/authorization');
const router = express.Router();
/**
* This returns the user if they are logged in.
*/
router.get('/', authorization.needed(), (req, res) => {
res.json(req.user);
});
router.delete('/', (req, res) => {
req.logout();
res.status(204).end();
/**
* This destroys the session of a user, if they have one.
*/
router.delete('/', authorization.needed(), (req, res) => {
req.session.destroy(() => {
res.status(204).end();
});
});
/**
* This sends back the user data as JSON.
*/
const HandleAuthCallback = (req, res, next) => (err, user) => {
if (err) {
return next(err);
}
if (!user) {
return next(authorization.ErrNotAuthorized);
}
// Perform the login of the user!
req.logIn(user, (err) => {
if (err) {
return next(err);
}
// We logged in the user! Let's send back the user data.
res.json({user});
});
};
/**
* Returns the response to the login attempt via a popup callback with some JS.
*/
const HandleAuthPopupCallback = (req, res, next) => (err, user) => {
if (err) {
return res.render('auth-callback', {err: JSON.stringify(err), data: null});
}
if (!user) {
return res.render('auth-callback', {err: JSON.stringify(authorization.ErrNotAuthorized), data: null});
}
// Perform the login of the user!
req.logIn(user, (err) => {
if (err) {
return res.render('auth-callback', {err: JSON.stringify(err), data: null});
}
// We logged in the user! Let's send back the user data.
res.render('auth-callback', {err: null, data: JSON.stringify(user)});
});
};
/**
* Local auth endpoint, will recieve a email and password
*/
router.post('/local', (req, res, next) => {
// Perform the local authentication.
passport.authenticate('local', (err, user) => {
if (err) {
return next(err);
}
passport.authenticate('local', HandleAuthCallback(req, res, next))(req, res, next);
});
if (!user) {
return next(authorization.ErrNotAuthorized);
}
/**
* Facebook auth endpoint, this will redirect the user immediatly to facebook
* for authorization.
*/
router.get('/facebook', passport.authenticate('facebook'));
// Perform the login of the user!
req.logIn(user, (err) => {
if (err) {
return next(err);
}
/**
* Facebook callback endpoint, this will send the user a html page designed to
* send back the user credentials upon sucesfull login.
*/
router.get('/facebook/callback', (req, res, next) => {
// We logged in the user! Let's send back the user data.
res.json({user});
});
})(req, res, next);
// Perform the facebook login flow and pass the data back through the opener.
passport.authenticate('facebook', HandleAuthPopupCallback(req, res, next))(req, res, next);
});
module.exports = router;
+1 -1
View File
@@ -94,7 +94,7 @@ router.post('/:comment_id', (req, res, next) => {
});
router.post('/:comment_id/status', (req, res, next) => {
Comment
.changeStatus(req.params.comment_id, req.body.status)
.then(comment => res.status(200).send(comment))
+9
View File
@@ -0,0 +1,9 @@
<!DOCTYPE html>
<html>
<body>
<script type="text/javascript">
window.opener.authCallback(<% if (err) { %>'<%- err %>'<% } else { %>null<% } %>, '<%- data %>');
setTimeout(function() { window.close(); }, 50);
</script>
</body>
</html>