Merge branch 'passport' of github.com:coralproject/talk into passport

This commit is contained in:
Belen Curcio
2016-11-11 19:42:20 -03:00
8 changed files with 221 additions and 81 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();
});
});
+36 -58
View File
@@ -12,6 +12,7 @@ const UserSchema = new mongoose.Schema({
required: true
},
displayName: String,
photo: String,
disabled: Boolean,
password: String,
profiles: [{
@@ -25,11 +26,6 @@ const UserSchema = new mongoose.Schema({
}
}],
roles: [String]
}, {
timestamps: {
createdAt: 'created_at',
updatedAt: 'updated_at'
}
});
// Add the indixies on the user profile data.
@@ -57,22 +53,6 @@ UserSchema.options.toJSON.transform = (doc, ret, options) => {
return ret;
};
/**
* toObject overrides to remove the password field from the toObject
* output.
*/
UserSchema.options.toObject = {};
UserSchema.options.toObject.hide = 'password';
UserSchema.options.toObject.transform = (doc, ret, options) => {
if (options.hide) {
options.hide.split(' ').forEach((prop) => {
delete ret[prop];
});
}
return ret;
};
/**
* Finds a user given their email address that we have for them in the system
* and ensures that the retuned user matches the password passed in as well.
@@ -121,21 +101,19 @@ UserSchema.statics.findLocalUser = function(email, password) {
UserSchema.statics.mergeUsers = function(dstUserID, srcUserID) {
let srcUser, dstUser;
return Promise
.all([
User.findOne({id: dstUserID}).exec(),
User.findOne({id: srcUserID}).exec()
])
.then((users) => {
dstUser = users[0];
srcUser = users[1];
return Promise.all([
User.findOne({id: dstUserID}).exec(),
User.findOne({id: srcUserID}).exec()
]).then((users) => {
dstUser = users[0];
srcUser = users[1];
srcUser.profiles.forEach((profile) => {
dstUser.profiles.push(profile);
});
srcUser.profiles.forEach((profile) => {
dstUser.profiles.push(profile);
});
return srcUser.remove();
})
return srcUser.remove();
})
.then(() => dstUser.save());
};
@@ -146,34 +124,34 @@ UserSchema.statics.mergeUsers = function(dstUserID, srcUserID) {
* @param {Function} done [description]
*/
UserSchema.statics.findOrCreateExternalUser = function(profile) {
return User
.findOne({
profiles: {
$elemMatch: {
return User.findOne({
profiles: {
$elemMatch: {
id: profile.id,
provider: profile.provider
}
}
})
.then((user) => {
if (user) {
return user;
}
// The user was not found, lets create them!
user = new User({
displayName: profile.displayName,
roles: [],
photo: Array.isArray(profile.photos) && profile.photos.length > 0 ? profile.photos[0].value : null,
profiles: [
{
id: profile.id,
provider: profile.provider
}
}
})
.then((user) => {
if (user) {
return user;
}
// The user was not found, lets create them!
user = new User({
displayName: profile.displayName,
roles: [],
profiles: [
{
id: profile.id,
provider: profile.provider
}
]
});
return user.save();
]
});
return user.save();
});
};
UserSchema.statics.changePassword = function(id, password) {
+2 -1
View File
@@ -65,7 +65,8 @@ 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`,
profileFields: ['id', 'displayName', 'picture.type(large)']
}, (accessToken, refreshToken, profile, done) => {
User
.findOrCreateExternalUser(profile)
+69 -18
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);
});
/**
* This destroys the session of a user, if they have one.
*/
router.delete('/', (req, res) => {
req.logout();
res.status(204).end();
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', {display: 'popup', authType: 'rerequest', scope: ['public_profile']}));
// 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
@@ -109,7 +109,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>