mirror of
https://github.com/jsbin/jsbin.git
synced 2026-02-01 16:46:05 +00:00
566 lines
18 KiB
JavaScript
566 lines
18 KiB
JavaScript
'use strict';
|
|
/**
|
|
* request.user vs request.session.user
|
|
* request.user is populated by middleware is is the result of a db query based on a user id/name
|
|
* request.session.user is the current logged in user
|
|
*/
|
|
var utils = require('../utils');
|
|
var errors = require('../errors');
|
|
var Observable = utils.Observable;
|
|
var passport = require('passport');
|
|
|
|
module.exports = Observable.extend({
|
|
constructor: function SessionHandler(sandbox) {
|
|
Observable.apply(this, arguments);
|
|
|
|
this.models = sandbox.models;
|
|
this.mailer = sandbox.mailer;
|
|
this.helpers = sandbox.helpers;
|
|
|
|
var methods = Object.getOwnPropertyNames(SessionHandler.prototype).filter(function (prop) {
|
|
return typeof this[prop] === 'function';
|
|
}, this);
|
|
|
|
utils.bindAll(this, methods);
|
|
},
|
|
loadUser: function (req, res, next) {
|
|
this.models.user.load(req.param('username'), function (err, user) {
|
|
if (err) {
|
|
return next(err);
|
|
}
|
|
if (user) {
|
|
req.user = user;
|
|
next();
|
|
} else {
|
|
next('User not found for ' + req.param('username'));
|
|
}
|
|
});
|
|
},
|
|
setProAccount: function(req, res){
|
|
var setPro = true;
|
|
this.models.user.setProAccount(req.session.user.name, setPro, function(){
|
|
var user = req.session.user;
|
|
user.pro = setPro;
|
|
this.setUserSession(req, user);
|
|
res.redirect('/');
|
|
}.bind(this));
|
|
},
|
|
loadUserFromSession: function (req, res, next) {
|
|
var name = req.session.user && req.session.user.name;
|
|
|
|
function notAuthorized() {
|
|
next(new errors.NotAuthorized());
|
|
}
|
|
|
|
if (!name) {
|
|
return notAuthorized();
|
|
}
|
|
|
|
this.models.user.load(name, function (err, user) {
|
|
if (err) {
|
|
return next(err);
|
|
}
|
|
|
|
if (!user) {
|
|
return notAuthorized();
|
|
}
|
|
|
|
req.user = user;
|
|
next();
|
|
});
|
|
},
|
|
loginUser: function (req, res) {
|
|
var model = this.models.user,
|
|
user = req.validatedUser;
|
|
|
|
if (!req.error) {
|
|
this.setUserSession(req, user);
|
|
}
|
|
|
|
if (req.ajax) {
|
|
if(req.error) {
|
|
res.json(400, req.error);
|
|
} else {
|
|
// Update the login timestamp but don't wait for a response as it's
|
|
// non crucial.
|
|
model.touchLogin(user.name, function () {});
|
|
|
|
res.json(200, {
|
|
referrer: (req.param('referrer') || ''),
|
|
user: user
|
|
});
|
|
}
|
|
} else {
|
|
if (req.error) {
|
|
return res.redirect('back');
|
|
}
|
|
res.redirect(req.param('referrer'));
|
|
}
|
|
},
|
|
logoutUser: function (req, res) {
|
|
delete req.session.user;
|
|
res.flash(req.flash.NOTIFICATION, 'You\'re now logged out.');
|
|
res.redirect(303, this.redirectUrl(req));
|
|
},
|
|
createUser: function (req, res, next) {
|
|
var params = {
|
|
name: req.param('name'),
|
|
key: req.param('key'),
|
|
email: req.param('email', '')
|
|
}, _this = this;
|
|
|
|
if (req.user) {
|
|
return this.respond(req, res, 400, {
|
|
ok: false,
|
|
message: 'Too late I\'m afraid, that username is taken.',
|
|
step: '2.3'
|
|
});
|
|
}
|
|
|
|
if (!params.name || !params.key) {
|
|
return this.respond(req, res, 400, {
|
|
ok: false,
|
|
message: 'Missing username or password',
|
|
step: '2.3'
|
|
});
|
|
}
|
|
|
|
this.models.user.create(params, function (err) {
|
|
if (err) {
|
|
return next(err);
|
|
}
|
|
|
|
// Add the user data to the session.
|
|
_this.setUserSession(req, params);
|
|
|
|
return _this.respond(req, res, 200, {ok: true, created: true});
|
|
});
|
|
},
|
|
updateUser: function (req, res, next) {
|
|
var requests = 0,
|
|
user = req.user,
|
|
email = req.param('email', '').trim(),
|
|
key = req.param('key', '').trim(),
|
|
_this = this;
|
|
|
|
if ('name' in req.params && req.user.name !== req.param('name')) {
|
|
return this.respond(req, res, 400, {
|
|
ok: false,
|
|
message: 'Sorry, changing your username isn\'t supported just yet. We\'re on it though!',
|
|
step: '6'
|
|
});
|
|
}
|
|
|
|
function onComplete(err) {
|
|
if (err) {
|
|
return next(err);
|
|
}
|
|
|
|
requests -= 1;
|
|
if (requests === 0) {
|
|
user.email = email;
|
|
_this.setUserSession(req, user);
|
|
|
|
_this.respond(req, res, 200, {
|
|
ok: true,
|
|
key: '',
|
|
avatar: req.session.user.avatar,
|
|
message: 'Account updated',
|
|
created: false
|
|
});
|
|
}
|
|
}
|
|
|
|
if (email && email !== user.email) {
|
|
requests += 1;
|
|
this.models.user.updateEmail(user.name, email, onComplete);
|
|
}
|
|
|
|
if (key) {
|
|
requests += 1;
|
|
this.models.user.updateKey(user.name, key, onComplete);
|
|
}
|
|
|
|
// Just return if nothing to update.
|
|
if (requests === 0) {
|
|
requests = 1;
|
|
onComplete();
|
|
}
|
|
},
|
|
requestToken: function (req, res) {
|
|
res.render('request', {
|
|
action: req.originalUrl,
|
|
csrf: req.session._csrf
|
|
});
|
|
},
|
|
// Generates a token to allow the user to reset their password.
|
|
forgotPassword: function (req, res, next) {
|
|
// Verify email.
|
|
var usernameOrEmailAddress = req.param('email', '').trim(),
|
|
helpers = this.helpers,
|
|
sessionHandler = this;
|
|
|
|
if (!usernameOrEmailAddress) {
|
|
res.statusCode = 400;
|
|
res.json({error: 'Either a username or email address is required to send a password reset token.'});
|
|
return;
|
|
}
|
|
|
|
var createToken = function createToken(user) {
|
|
this.models.forgotToken.createToken(user.name, function (err, token) {
|
|
if (err) {
|
|
return next(err);
|
|
}
|
|
|
|
var ctx = {
|
|
domain: helpers.set('url host').split(':')[0],
|
|
link: helpers.url('reset?token=' + token, true)
|
|
};
|
|
|
|
// Send email.
|
|
sessionHandler.mailer.forgotPassword(user.email, ctx, function (err) {
|
|
var flash = 'An email has been sent to ' + user.email,
|
|
type = req.flash.NOTIFICATION;
|
|
|
|
if (err && err instanceof errors.MailerError) {
|
|
type = req.flash.ERROR;
|
|
if (sessionHandler.mailer.isEnabled()) {
|
|
flash = 'Sorry, an error occurred when sending the reset email: ' + err;
|
|
} else {
|
|
flash = 'Unable to send reset, email is not enabled in this version of JS Bin.';
|
|
}
|
|
} else if (err) {
|
|
return next(err);
|
|
}
|
|
|
|
if (req.ajax) {
|
|
res.json({ message: flash });
|
|
} else {
|
|
res.flash(type, flash);
|
|
res.redirect(303, helpers.url());
|
|
}
|
|
});
|
|
});
|
|
}.bind(this);
|
|
|
|
var noUserFound = function () {
|
|
res.statusCode = 404;
|
|
res.json({error: 'I looked, but I couldn\'t find any user with those details. Sorry!'});
|
|
};
|
|
|
|
// try loading by username first
|
|
this.models.user.load(usernameOrEmailAddress, function (err, user) {
|
|
if (err) {
|
|
return next(err);
|
|
}
|
|
|
|
if (!user) {
|
|
// try by email next
|
|
this.models.user.loadByEmail(usernameOrEmailAddress, function (err, user) {
|
|
if (!user) {
|
|
return noUserFound();
|
|
} else {
|
|
createToken(user);
|
|
}
|
|
});
|
|
} else {
|
|
createToken(user);
|
|
}
|
|
}.bind(this));
|
|
},
|
|
resetPassword: function (req, res) {
|
|
// Validate the token.
|
|
var token = req.param('token', '').trim(),
|
|
_this = this;
|
|
|
|
if (!token) {
|
|
// Nope.
|
|
return res.redirect(this.helpers.url());
|
|
}
|
|
|
|
this.models.forgotToken.loadUser(token, function (err, user) {
|
|
if (err) {
|
|
res.flash(req.flash.ERROR, 'Could not load user.');
|
|
return res.redirect(303, _this.helpers.url());
|
|
}
|
|
|
|
if (!user) {
|
|
return res.redirect(_this.helpers.url());
|
|
}
|
|
|
|
// Log the user in.
|
|
_this.setUserSession(req, user);
|
|
|
|
// Clear all their tokens.
|
|
_this.models.forgotToken.expireTokensByUser(user.name, function () {});
|
|
|
|
res.flash(req.flash.NOTIFICATION, 'You\'re now logged in, you can change your password in the account menu');
|
|
res.redirect(303, _this.helpers.url());
|
|
});
|
|
},
|
|
setUserSession: function (req, user) {
|
|
var settings = user.settings || {};
|
|
|
|
if (typeof settings === 'string') {
|
|
try {
|
|
settings = JSON.parse(settings);
|
|
} catch (e) {
|
|
settings = {};
|
|
}
|
|
}
|
|
req.session.user = {
|
|
avatar: utils.gravatar(user.email),
|
|
name: user.name,
|
|
email: user.email,
|
|
github_token: user.github_token, // jshint ignore:line
|
|
lastLogin: user.last_login || new Date(), // jshint ignore:line
|
|
pro: user.pro,
|
|
settings: settings
|
|
};
|
|
},
|
|
|
|
checkUserLoggedIn: function(req, res, next) {
|
|
if (req.session.user) {
|
|
return res.redirect('/');
|
|
}
|
|
next();
|
|
},
|
|
|
|
// Routes all post requests to /sethome which needs some serious discussion
|
|
// as at the moment it uses the presence of params to determine which action
|
|
// to call. It is consistent with the PHP app though.
|
|
routeSetHome: function (req, res, next) {
|
|
var params = utils.extract(req.body, 'name', 'key', 'email'),
|
|
_this = this;
|
|
|
|
this.loadUserFromSession(req, res, function (err) {
|
|
if (!err) {
|
|
// User was loaded from the session (they're logged in).
|
|
_this.updateUser(req, res, next);
|
|
} else if (err instanceof errors.NotAuthorized) {
|
|
// This is a NotAuthorized error which means there is no session
|
|
// so this user is not logged in. Now we can try and load the user
|
|
// based on the 'name' key.
|
|
_this.loadUser(req, res, function (err) {
|
|
if (err) {
|
|
return next(err);
|
|
}
|
|
|
|
if ('email' in params) {
|
|
// There's an email so this is a registration.
|
|
_this.createUser(req, res, next);
|
|
} else {
|
|
// No email, this is a login.
|
|
_this.loginUser(req, res, next);
|
|
}
|
|
});
|
|
} else {
|
|
return next(err);
|
|
}
|
|
});
|
|
},
|
|
respond: function (req, res, status, data) {
|
|
if (res.ajax) {return res.json(status, data); }
|
|
|
|
var type = status && status < 400 ? res.flash.INFO : res.flash.ERROR,
|
|
redirect = type === req.flash.INFO ? this.redirectUrl(req) : 'back';
|
|
res.flash(type, data.message);
|
|
res.redirect(303, redirect);
|
|
},
|
|
redirectUrl: function (req, fallback) {
|
|
var url = req.param('_redirect', fallback || 'back');
|
|
// We only want to redirect to internal urls.
|
|
if (url.indexOf('http') > -1) {
|
|
url = 'home';
|
|
}
|
|
return url;
|
|
},
|
|
|
|
respondWithMessage: function (res, type, message) {
|
|
res.flash(type, message);
|
|
return res.redirect('/');
|
|
},
|
|
|
|
/**
|
|
* DropBox Auth
|
|
*/
|
|
|
|
dropboxAuth: passport.authenticate('dropbox-oauth2'),
|
|
|
|
dropboxPassportCallback: passport.authenticate('dropbox-oauth2', {
|
|
failureRedirect: '/'
|
|
}),
|
|
|
|
dropboxCallback: function (req, res, next) {
|
|
if (req.session.user) {
|
|
req.session.user.dropbox_token = req.user.accessToken;
|
|
}
|
|
res.flash(req.flash.NOTIFICATION, 'Your dropbox account has been linked to JS Bin, all future bins will now be saved to dropbox');
|
|
res.redirect('/');
|
|
},
|
|
|
|
/**
|
|
* Github Auth
|
|
*/
|
|
|
|
/**
|
|
* First part of GitHub auth. Uses the GitHub Strategy (passport-github, see
|
|
* app.js) to do an OAuth2 exchange with GitHub.
|
|
*/
|
|
github: [
|
|
function (req, res, next) {
|
|
req.session.referer = req.headers.referer;
|
|
next();
|
|
},
|
|
passport.authenticate('github', { scope: ['user:email', 'gist'] })
|
|
],
|
|
|
|
/**
|
|
* Passport layer for the auth callback (/auth/github/callback). Does the
|
|
* final token exchange
|
|
*/
|
|
githubPassportCallback: passport.authenticate('github', {
|
|
failureRedirect: '/'
|
|
}),
|
|
|
|
/**
|
|
* Authentication through Github is complete, handle the authenticated user
|
|
* - Find user from GitHub id
|
|
- Found?
|
|
- Signed out
|
|
- Sign in as found user
|
|
- Signed in
|
|
- JSBin ids match?
|
|
- Weird case, sign in and allow through
|
|
- JSBin ids don't match?
|
|
- 'Sorry, that GitHub account is linked to another JS Bin account.'
|
|
- Not found?
|
|
- Signed out
|
|
- Find by github name
|
|
- Found?
|
|
- 'Please sign in to link accounts.'
|
|
- Not found?
|
|
- Create user
|
|
- Signed in
|
|
- Save GitHub data to user account & session
|
|
*/
|
|
githubCallback: function(req, res) {
|
|
var model = this.models.user;
|
|
var githubUser = req.user,
|
|
sessionUser = req.session.user;
|
|
|
|
var redirect = req.session.referer || '/';
|
|
|
|
// Find user from GitHub id
|
|
this.models.user.getOne('getUserByGithubId', [githubUser.profile.id], function (err, jsbinUser) {
|
|
if (err) {
|
|
return this.respondWithMessage(res, res.flash.ERROR, 'Sorry, there was an error while accessing the database with your GitHub id (' + err + ')');
|
|
}
|
|
// Was a user found?
|
|
if (jsbinUser) {
|
|
// Is the user signed in?
|
|
if (sessionUser) {
|
|
// Do the JS Bin ids match? (weird edge case where somebody links twice)
|
|
if (jsbinUser.name === sessionUser.name) {
|
|
// Sign in and allow through
|
|
this.setUserSession(req, jsbinUser);
|
|
// Update the login timestamp
|
|
model.touchLogin(jsbinUser.name, function () {});
|
|
return res.redirect(redirect);
|
|
}
|
|
// IDs did not match.
|
|
res.flash(res.flash.ERROR, 'Sorry, that GitHub account is linked to another JS Bin account.');
|
|
return res.redirect(redirect);
|
|
}
|
|
|
|
// User is not signed in
|
|
// Sign them back in as the jsbin user, updating their token
|
|
jsbinUser.github_token = githubUser.access_token; // jshint ignore:line
|
|
return this.models.user.updateGithubData(jsbinUser.name, jsbinUser.github_id, jsbinUser.github_token, function (err) { // jshint ignore:line
|
|
if (err) {
|
|
return this.respondWithMessage(res, res.flash.ERROR, 'Sorry, could not update your GitHub data & sign you in (' + err + ')');
|
|
}
|
|
// Add the user data to the session.
|
|
this.setUserSession(req, jsbinUser);
|
|
// Update the login timestamp
|
|
model.touchLogin(jsbinUser.name, function () {});
|
|
res.flash(res.flash.INFO, 'Welcome back.');
|
|
return res.redirect(redirect);
|
|
}.bind(this));
|
|
}
|
|
|
|
// Ok, no id matching JS Bin user was found.
|
|
// Is the user signed in?
|
|
if (sessionUser) {
|
|
sessionUser.github_token = githubUser.access_token; // jshint ignore:line
|
|
sessionUser.github_id = githubUser.profile.id; // jshint ignore:line
|
|
// Hook the two users up
|
|
return this.models.user.updateGithubData(sessionUser.name, sessionUser.github_id, sessionUser.github_token, function (err) { // jshint ignore:line
|
|
if (err) {
|
|
return this.respondWithMessage(res, res.flash.ERROR, 'Sorry, could not connect your GitHub and JS Bin accounts (' + err + ')');
|
|
}
|
|
// Add the user data to the session.
|
|
this.setUserSession(req, sessionUser);
|
|
// Update the login timestamp
|
|
model.touchLogin(sessionUser.name, function () {});
|
|
res.flash(res.flash.NOTIFICATION, 'Your JS Bin account ('+sessionUser.name+') has been linked to your GitHub account ('+githubUser.profile.username+'). Nice.');
|
|
return res.redirect(redirect);
|
|
}.bind(this));
|
|
}
|
|
|
|
// No id matching user was found and the user is not signed in
|
|
// Find by GitHub name
|
|
this.models.user.load(githubUser.profile.username, function (err, jsbinUserFromGithubName) {
|
|
if (err) {
|
|
return this.respondWithMessage(res, res.flash.ERROR, 'Sorry, there was an error while accessing the database with your GitHub username (' + err + ')');
|
|
}
|
|
// Was a name matching user found
|
|
if (jsbinUserFromGithubName) {
|
|
// Uh oh, that user exists already
|
|
res.flash(res.flash.ERROR, 'Your GitHub username is already used in JS Bin. If this is you, please sign into JS Bin and "link your GitHub account" otherwise, register, then link your account.');
|
|
return res.redirect(redirect);
|
|
}
|
|
// No matching user was found, create an account!
|
|
// Generate them a random password
|
|
require('crypto').randomBytes(48, function(ex, buf) {
|
|
var userData = {
|
|
name: githubUser.profile.username,
|
|
key: buf.toString('hex'),
|
|
email: githubUser.profile.email || '',
|
|
github_id: githubUser.profile.id, // jshint ignore:line
|
|
github_token: githubUser.access_token, // jshint ignore:line
|
|
};
|
|
this.models.user.create(userData, function (err) {
|
|
if (err) {
|
|
return this.respondWithMessage(res, res.flash.ERROR, 'Sorry, there was an error while creating a new user (' + err + ')');
|
|
}
|
|
|
|
// Add the user data to the session.
|
|
this.setUserSession(req, userData);
|
|
// Update the login timestamp
|
|
model.touchLogin(userData.name, function () {});
|
|
res.flash(res.flash.NOTIFICATION, 'Welcome to JS Bin, '+userData.name+'.');
|
|
res.redirect(redirect);
|
|
}.bind(this));
|
|
}.bind(this));
|
|
}.bind(this));
|
|
}.bind(this));
|
|
},
|
|
|
|
// Handle custom errors raised by passport-github when recieving an invalid
|
|
// code parameter to the callback url eg: /auth/github/callback?code=foo
|
|
//
|
|
// This will likely occur if a user refreshes or hits the back button on
|
|
// an expired url. Here we handle it gracefully with a flash message and a
|
|
// redirect back to the app.
|
|
githubCallbackWithError: function (err, req, res, next) {
|
|
// Handle errors raised by the passport-github module indicated by the
|
|
// custom ouathError property. For the moment only handle 401 codes and
|
|
// let others bubble up to the global handler.
|
|
if (err.oauthError && err.oauthError.statusCode === 401) {
|
|
return this.respondWithMessage(res, res.flash.ERROR, 'Sorry, looks like that auth code has expired, please try and login again.');
|
|
}
|
|
next(err);
|
|
}
|
|
});
|