fix(signup): signup auth (#15628)

* fix(models.user): Colocate all user methods

Moved user methods/extensions into one file. Tracked down `next method
called more than once` error and setting headers after their sent. Let
regular error handler handle api errors as well.

* feat(server.auth): Disable github account creation

We are no longer allowing account creation through github

* refactor(Auth): Move user identity link into models dir

* feat(Disable link account login): This removes the ability to use a linked account t

* feat(errorhandlers): Add opbeat, filter out handled error
This commit is contained in:
Berkeley Martinez
2017-07-13 11:39:07 -07:00
committed by mrugesh mohapatra
parent 7805d74ea7
commit 2fcd976700
17 changed files with 591 additions and 497 deletions

View File

@ -0,0 +1,88 @@
import { Observable } from 'rx';
import debug from 'debug';
import { observeMethod, observeQuery } from '../../server/utils/rx';
import {
createUserUpdatesFromProfile,
getSocialProvider
} from '../../server/utils/auth';
const log = debug('fcc:models:UserCredential');
module.exports = function(UserCredential) {
UserCredential.link = function(
userId,
_provider,
authScheme,
profile,
credentials,
options = {},
cb
) {
if (typeof options === 'function' && !cb) {
cb = options;
options = {};
}
const User = UserCredential.app.models.User;
const findCred = observeMethod(UserCredential, 'findOne');
const createCred = observeMethod(UserCredential, 'create');
const provider = getSocialProvider(_provider);
const query = {
where: {
provider: provider,
externalId: profile.id
}
};
// find createCred if they exist
// if not create it
// if yes, update credentials
// also if github
// update profile
// update username
// update picture
log('link query', query);
return findCred(query)
.flatMap(_credentials => {
const modified = new Date();
const updateUser = User.update$(
{ id: userId },
createUserUpdatesFromProfile(provider, profile)
);
let updateCredentials;
if (!_credentials) {
updateCredentials = createCred({
provider,
externalId: profile.id,
authScheme,
// we no longer want to keep the profile
// this is information we do not need or use
profile: null,
credentials,
userId,
created: modified,
modified
});
}
_credentials.credentials = credentials;
updateCredentials = observeQuery(
_credentials,
'updateAttributes',
{
profile: null,
credentials,
modified
}
);
return Observable.combineLatest(
updateUser,
updateCredentials,
(_, credentials) => credentials
);
})
.subscribe(
credentials => cb(null, credentials),
cb
);
};
};

View File

@ -1,172 +1,131 @@
import loopback from 'loopback'; import { Observable } from 'rx';
import debugFactory from 'debug'; // import debug from 'debug';
import dedent from 'dedent';
import { import {
setProfileFromGithub, getSocialProvider,
getFirstImageFromProfile,
getUsernameFromProvider, getUsernameFromProvider,
getSocialProvider createUserUpdatesFromProfile
} from '../../server/utils/auth'; } from '../../server/utils/auth';
import { defaultProfileImage } from '../utils/constantStrings.json'; import { observeMethod, observeQuery } from '../../server/utils/rx';
import { wrapHandledError } from '../../server/utils/create-handled-error.js';
const githubRegex = (/github/i); // const log = debug('fcc:models:userIdent');
const debug = debugFactory('fcc:models:userIdent');
export default function(UserIdent) { export default function(UserIdent) {
UserIdent.on('dataSourceAttached', () => {
UserIdent.findOne$ = observeMethod(UserIdent, 'findOne');
});
// original source // original source
// github.com/strongloop/loopback-component-passport // github.com/strongloop/loopback-component-passport
const createAccountMessage = // find identity if it exist
'Accounts can only be created using GitHub or though email'; // if not redirect to email signup
// if yes and github
// update profile
// update username
// update picture
UserIdent.login = function( UserIdent.login = function(
provider, _provider,
authScheme, authScheme,
profile, profile,
credentials, credentials,
options, options,
cb cb
) { ) {
const User = UserIdent.app.models.User;
const AccessToken = UserIdent.app.models.AccessToken;
const provider = getSocialProvider(_provider);
options = options || {}; options = options || {};
if (typeof options === 'function' && !cb) { if (typeof options === 'function' && !cb) {
cb = options; cb = options;
options = {}; options = {};
} }
const userIdentityModel = UserIdent;
profile.id = profile.id || profile.openid; profile.id = profile.id || profile.openid;
const filter = { const query = {
where: { where: {
provider: getSocialProvider(provider), provider: provider,
externalId: profile.id externalId: profile.id
} },
include: 'user'
}; };
return userIdentityModel.findOne(filter) return UserIdent.findOne$(query)
.then(identity => { .flatMap(identity => {
if (!identity) {
throw wrapHandledError(
new Error('user identity account not found'),
{
message: dedent`
New accounts can only be created using an email address.
Please create an account below
`,
type: 'info',
redirectTo: '/signup'
}
);
}
const modified = new Date();
const user = identity.user();
if (!user) {
const username = getUsernameFromProvider(provider, profile);
return observeQuery(
identity,
'updateAttributes',
{
isOrphaned: username || true
}
)
.do(() => {
throw wrapHandledError(
new Error('user identity is not associated with a user'),
{
type: 'info',
redirectTo: '/signup',
message: dedent`
The user account associated with the ${provider} user ${username || 'Anon'}
no longer exists.
`
}
);
});
}
const updateUser = User.update$(
{ id: user.id },
createUserUpdatesFromProfile(provider, profile)
).map(() => user);
// identity already exists // identity already exists
// find user and log them in // find user and log them in
if (identity) { identity.credentials = credentials;
identity.credentials = credentials; const attributes = {
const options = { // we no longer want to keep the profile
profile: profile, // this is information we do not need or use
credentials: credentials, profile: null,
modified: new Date() credentials: credentials,
}; modified
return identity.updateAttributes(options) };
// grab user associated with identity const updateIdentity = observeQuery(
.then(() => identity.user()) identity,
.then(user => { 'updateAttributes',
// Create access token for user attributes
const options = { );
created: new Date(), const createToken = observeQuery(
ttl: user.constructor.settings.ttl AccessToken,
}; 'create',
return user.accessTokens.create(options) {
.then(token => ({ user, token })); userId: user.id,
}) created: new Date(),
.then(({ token, user })=> { ttl: user.constructor.settings.ttl
cb(null, user, identity, token); }
}) );
.catch(err => cb(err)); return Observable.combineLatest(
} updateUser,
// Find the user model updateIdentity,
const userModel = userIdentityModel.relations.user && createToken,
userIdentityModel.relations.user.modelTo || (user, identity, token) => ({ user, identity, token })
loopback.getModelByType(loopback.User); );
})
const userObj = options.profileToUser(provider, profile, options); .subscribe(
if (getSocialProvider(provider) !== 'github') { ({ user, identity, token }) => cb(null, user, identity, token),
const err = new Error(createAccountMessage); cb
err.userMessage = createAccountMessage; );
err.messageType = 'info';
err.redirectTo = '/signin';
return process.nextTick(() => cb(err));
}
let query;
if (userObj.email) {
query = { or: [
{ username: userObj.username },
{ email: userObj.email }
]};
} else {
query = { username: userObj.username };
}
return userModel.findOrCreate({ where: query }, userObj)
.then(([ user ]) => {
const promises = [
userIdentityModel.create({
provider: getSocialProvider(provider),
externalId: profile.id,
authScheme: authScheme,
profile: profile,
credentials: credentials,
userId: user.id,
created: new Date(),
modified: new Date()
}),
user.accessTokens.create({
created: new Date(),
ttl: user.constructor.settings.ttl
})
];
return Promise.all(promises)
.then(([ identity, token ]) => ({ user, identity, token }));
})
.then(({ user, token, identity }) => cb(null, user, identity, token))
.catch(err => cb(err));
});
}; };
UserIdent.observe('before save', function(ctx, next) {
const userIdent = ctx.currentInstance || ctx.instance;
if (!userIdent) {
debug('no user identity instance found');
return next();
}
return userIdent.user(function(err, user) {
let userChanged = false;
if (err) { return next(err); }
if (!user) {
debug('no user attached to identity!');
return next();
}
const { profile, provider } = userIdent;
const picture = getFirstImageFromProfile(profile);
debug('picture', picture, user.picture);
// check if picture was found
// check if user has no picture
// check if user has default picture
// set user.picture from oauth provider
if (
picture &&
(!user.picture || user.picture === defaultProfileImage)
) {
debug('setting user picture');
user.picture = picture;
userChanged = true;
}
if (!githubRegex.test(provider) && profile) {
user[provider] = getUsernameFromProvider(provider, profile);
userChanged = true;
}
// if user signed in with github refresh their info
if (githubRegex.test(provider) && profile && profile._json) {
debug("user isn't github cool or username from github is different");
setProfileFromGithub(user, profile, profile._json);
userChanged = true;
}
if (userChanged) {
return user.save(function(err) {
if (err) { return next(err); }
return next();
});
}
debug('exiting after user identity before save');
return next();
});
});
} }

View File

@ -6,14 +6,26 @@ import debugFactory from 'debug';
import { isEmail } from 'validator'; import { isEmail } from 'validator';
import path from 'path'; import path from 'path';
import { saveUser, observeMethod } from '../../server/utils/rx'; import { saveUser, observeMethod } from '../../server/utils/rx.js';
import { blacklistedUsernames } from '../../server/utils/constants'; import { blacklistedUsernames } from '../../server/utils/constants.js';
import { wrapHandledError } from '../../server/utils/create-handled-error.js';
const debug = debugFactory('fcc:user:remote'); const debug = debugFactory('fcc:user:remote');
const BROWNIEPOINTS_TIMEOUT = [1, 'hour']; const BROWNIEPOINTS_TIMEOUT = [1, 'hour'];
const isDev = process.env.NODE_ENV !== 'production'; const isDev = process.env.NODE_ENV !== 'production';
const devHost = process.env.HOST || 'localhost'; const devHost = process.env.HOST || 'localhost';
const createEmailError = () => new Error(
'Please check to make sure the email is a valid email address.'
);
function destroyAll(id, Model) {
return Observable.fromNodeCallback(
Model.destroyAll,
Model
)({ userId: id });
}
function getAboutProfile({ function getAboutProfile({
username, username,
githubProfile: github, githubProfile: github,
@ -64,10 +76,99 @@ module.exports = function(User) {
User.count$ = Observable.fromNodeCallback(User.count, User); User.count$ = Observable.fromNodeCallback(User.count, User);
}); });
User.beforeRemote('create', function({ req }) {
const body = req.body;
// note(berks): we now require all new users to supply an email
// this was not always the case
if (
typeof body.email !== 'string' ||
!isEmail(body.email)
) {
return Promise.reject(createEmailError());
}
// assign random username to new users
// actual usernames will come from github
body.username = 'fcc' + uuid.v4();
if (body) {
// this is workaround for preventing a server crash
// we do this on create and on save
// refer strongloop/loopback/#1364
if (body.password === '') {
body.password = null;
}
// set email verified false on user email signup
// should not be set with oauth signin methods
body.emailVerified = false;
}
return User.doesExist(null, body.email)
.catch(err => {
throw wrapHandledError(err, { redirectTo: '/email-signup' });
})
.then(exists => {
if (!exists) {
return null;
}
const err = wrapHandledError(
new Error('user already exists'),
{
redirectTo: '/email-signin',
message: dedent`
The ${body.email} email address is already associated with an account.
Try signing in with it here instead.
`
}
);
throw err;
});
});
// send welcome email to new camper
User.afterRemote('create', function({ req, res }, user, next) {
debug('user created, sending email');
if (!user.email || !isEmail(user.email)) { return next(); }
const redirect = req.session && req.session.returnTo ?
req.session.returnTo :
'/';
var mailOptions = {
type: 'email',
to: user.email,
from: 'team@freecodecamp.com',
subject: 'Welcome to freeCodeCamp!',
protocol: isDev ? null : 'https',
host: isDev ? devHost : 'freecodecamp.com',
port: isDev ? null : 443,
template: path.join(
__dirname,
'..',
'..',
'server',
'views',
'emails',
'a-extend-user-welcome.ejs'
),
redirect: '/email-signin'
};
debug('sending welcome email');
return user.verify(mailOptions, function(err) {
if (err) { return next(err); }
req.flash('success', {
msg: [ 'Congratulations ! We\'ve created your account. ',
'Please check your email. We sent you a link that you can ',
'click to verify your email address and then login.'
].join('')
});
return res.redirect(redirect);
});
});
User.observe('before save', function({ instance: user }, next) { User.observe('before save', function({ instance: user }, next) {
if (user) { if (user) {
// Some old accounts will not have emails associated with theme
// we verify only if the email field is populated
if (user.email && !isEmail(user.email)) { if (user.email && !isEmail(user.email)) {
return next(new Error('Email format is not valid')); return next(createEmailError());
} }
user.username = user.username.trim().toLowerCase(); user.username = user.username.trim().toLowerCase();
user.email = typeof user.email === 'string' ? user.email = typeof user.email === 'string' ?
@ -82,6 +183,7 @@ module.exports = function(User) {
user.progressTimestamps.push({ timestamp: Date.now() }); user.progressTimestamps.push({ timestamp: Date.now() });
} }
// this is workaround for preventing a server crash // this is workaround for preventing a server crash
// we do this on save and on create
// refer strongloop/loopback/#1364 // refer strongloop/loopback/#1364
if (user.password === '') { if (user.password === '') {
user.password = null; user.password = null;
@ -90,6 +192,40 @@ module.exports = function(User) {
return next(); return next();
}); });
// remove lingering user identities before deleting user
User.observe('before delete', function(ctx, next) {
const UserIdentity = User.app.models.UserIdentity;
const UserCredential = User.app.models.UserCredential;
debug('removing user', ctx.where);
var id = ctx.where && ctx.where.id ? ctx.where.id : null;
if (!id) {
return next();
}
return Observable.combineLatest(
destroyAll(id, UserIdentity),
destroyAll(id, UserCredential),
function(identData, credData) {
return {
identData: identData,
credData: credData
};
}
)
.subscribe(
function(data) {
debug('deleted', data);
},
function(err) {
debug('error deleting user %s stuff', id, err);
next(err);
},
function() {
debug('user stuff deleted for user %s', id);
next();
}
);
});
debug('setting up user hooks'); debug('setting up user hooks');
User.beforeRemote('confirm', function(ctx, _, next) { User.beforeRemote('confirm', function(ctx, _, next) {
@ -153,41 +289,9 @@ module.exports = function(User) {
return ctx.res.redirect(redirect); return ctx.res.redirect(redirect);
}); });
User.beforeRemote('create', function({ req, res }, _, next) {
req.body.username = 'fcc' + uuid.v4().slice(0, 8);
if (!req.body.email) {
return next();
}
if (!isEmail(req.body.email)) {
return next(new Error('Email format is not valid'));
}
return User.doesExist(null, req.body.email)
.then(exists => {
if (!exists) {
return next();
}
req.flash('error', {
msg: dedent`
The ${req.body.email} email address is already associated with an account.
Try signing in with it here instead.
`
});
return res.redirect('/email-signin');
})
.catch(err => {
console.error(err);
req.flash('error', {
msg: 'Oops, something went wrong, please try again later'
});
return res.redirect('/email-signup');
});
});
User.on('resetPasswordRequest', function(info) { User.on('resetPasswordRequest', function(info) {
if (!isEmail(info.email)) { if (!isEmail(info.email)) {
console.error(new Error('Email format is not valid')); console.error(createEmailError());
return null; return null;
} }
let url; let url;
@ -232,7 +336,7 @@ module.exports = function(User) {
const { body } = ctx.req; const { body } = ctx.req;
if (body && typeof body.email === 'string') { if (body && typeof body.email === 'string') {
if (!isEmail(body.email)) { if (!isEmail(body.email)) {
return next(new Error('Email format is not valid')); return next(createEmailError());
} }
body.email = body.email.toLowerCase(); body.email = body.email.toLowerCase();
} }
@ -392,9 +496,7 @@ module.exports = function(User) {
true; true;
if (!isEmail('' + email)) { if (!isEmail('' + email)) {
return Observable.throw( return Observable.throw(createEmailError());
new Error('The submitted email not valid.')
);
} }
// email is already associated and verified with this account // email is already associated and verified with this account
if (ownEmail && this.emailVerified) { if (ownEmail && this.emailVerified) {
@ -588,11 +690,13 @@ module.exports = function(User) {
User.prototype.updateTheme = function updateTheme(theme) { User.prototype.updateTheme = function updateTheme(theme) {
if (!this.constructor.themes[theme]) { if (!this.constructor.themes[theme]) {
const err = new Error( const err = wrapHandledError(
'Theme is not valid.' new Error('Theme is not valid.'),
{
Type: 'info',
message: err.message
}
); );
err.messageType = 'info';
err.userMessage = err.message;
return Promise.reject(err); return Promise.reject(err);
} }
return this.update$({ theme }) return this.update$({ theme })

136
package-lock.json generated
View File

@ -124,6 +124,11 @@
"integrity": "sha1-q11PuIP1loFtNRX495HAr0ht1ic=", "integrity": "sha1-q11PuIP1loFtNRX495HAr0ht1ic=",
"dev": true "dev": true
}, },
"after-all-results": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/after-all-results/-/after-all-results-2.0.0.tgz",
"integrity": "sha1-asL8ICtQD4jaj09VMM+hAPTGotA="
},
"ajv": { "ajv": {
"version": "4.11.8", "version": "4.11.8",
"resolved": "https://registry.npmjs.org/ajv/-/ajv-4.11.8.tgz", "resolved": "https://registry.npmjs.org/ajv/-/ajv-4.11.8.tgz",
@ -303,6 +308,11 @@
"resolved": "https://registry.npmjs.org/async/-/async-2.1.5.tgz", "resolved": "https://registry.npmjs.org/async/-/async-2.1.5.tgz",
"integrity": "sha1-5YfGhYCZSsZ/xW/4bTrFa9voELw=" "integrity": "sha1-5YfGhYCZSsZ/xW/4bTrFa9voELw="
}, },
"async-cache": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/async-cache/-/async-cache-1.1.0.tgz",
"integrity": "sha1-SppaidBl7F2OUlS9nulrp2xTK1o="
},
"async-each": { "async-each": {
"version": "1.0.1", "version": "1.0.1",
"resolved": "https://registry.npmjs.org/async-each/-/async-each-1.0.1.tgz", "resolved": "https://registry.npmjs.org/async-each/-/async-each-1.0.1.tgz",
@ -1737,6 +1747,11 @@
"resolved": "https://registry.npmjs.org/console-browserify/-/console-browserify-1.1.0.tgz", "resolved": "https://registry.npmjs.org/console-browserify/-/console-browserify-1.1.0.tgz",
"integrity": "sha1-8CQcRXMKn8YyOyBtvzjtx0HQuxA=" "integrity": "sha1-8CQcRXMKn8YyOyBtvzjtx0HQuxA="
}, },
"console-log-level": {
"version": "1.4.0",
"resolved": "https://registry.npmjs.org/console-log-level/-/console-log-level-1.4.0.tgz",
"integrity": "sha1-QDWBi+6jflhQoMA8jUUMpfLNEhc="
},
"constantinople": { "constantinople": {
"version": "3.0.2", "version": "3.0.2",
"resolved": "https://registry.npmjs.org/constantinople/-/constantinople-3.0.2.tgz", "resolved": "https://registry.npmjs.org/constantinople/-/constantinople-3.0.2.tgz",
@ -2442,6 +2457,11 @@
"integrity": "sha1-uJbiOp5ei6M4cfyZar02NfyaHH0=", "integrity": "sha1-uJbiOp5ei6M4cfyZar02NfyaHH0=",
"dev": true "dev": true
}, },
"error-callsites": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/error-callsites/-/error-callsites-1.0.1.tgz",
"integrity": "sha1-QoYWmt+PwSSC9VYRFyTFrthzppI="
},
"error-ex": { "error-ex": {
"version": "1.3.1", "version": "1.3.1",
"resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.1.tgz", "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.1.tgz",
@ -2875,6 +2895,11 @@
"integrity": "sha1-PYpcZog6FqMMqGQ+hR8Zuqd5eRc=", "integrity": "sha1-PYpcZog6FqMMqGQ+hR8Zuqd5eRc=",
"dev": true "dev": true
}, },
"fast-safe-stringify": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/fast-safe-stringify/-/fast-safe-stringify-1.2.0.tgz",
"integrity": "sha1-69QmZv0Y/k8rpPDSlQZfP4XK3pY="
},
"fbjs": { "fbjs": {
"version": "0.8.12", "version": "0.8.12",
"resolved": "https://registry.npmjs.org/fbjs/-/fbjs-0.8.12.tgz", "resolved": "https://registry.npmjs.org/fbjs/-/fbjs-0.8.12.tgz",
@ -4395,6 +4420,11 @@
} }
} }
}, },
"hashlru": {
"version": "2.2.0",
"resolved": "https://registry.npmjs.org/hashlru/-/hashlru-2.2.0.tgz",
"integrity": "sha1-eTpYlD+QKupXgXfXsDNfE/JpS3E="
},
"hawk": { "hawk": {
"version": "3.1.3", "version": "3.1.3",
"resolved": "https://registry.npmjs.org/hawk/-/hawk-3.1.3.tgz", "resolved": "https://registry.npmjs.org/hawk/-/hawk-3.1.3.tgz",
@ -4578,6 +4608,11 @@
"resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz",
"integrity": "sha1-khi5srkoojixPcT7a21XbyMUU+o=" "integrity": "sha1-khi5srkoojixPcT7a21XbyMUU+o="
}, },
"in-publish": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/in-publish/-/in-publish-2.0.0.tgz",
"integrity": "sha1-4g/146KvwmkDILbcVSaCqcf631E="
},
"indent-string": { "indent-string": {
"version": "2.1.0", "version": "2.1.0",
"resolved": "https://registry.npmjs.org/indent-string/-/indent-string-2.1.0.tgz", "resolved": "https://registry.npmjs.org/indent-string/-/indent-string-2.1.0.tgz",
@ -4752,12 +4787,27 @@
"integrity": "sha1-0Jb5JqPe1WAPP9/ZEZjLCIjC2GM=", "integrity": "sha1-0Jb5JqPe1WAPP9/ZEZjLCIjC2GM=",
"dev": true "dev": true
}, },
"is-integer": {
"version": "1.0.7",
"resolved": "https://registry.npmjs.org/is-integer/-/is-integer-1.0.7.tgz",
"integrity": "sha1-a96Bqs3feLZZtmKdYpytxRqIbVw="
},
"is-my-json-valid": { "is-my-json-valid": {
"version": "2.16.0", "version": "2.16.0",
"resolved": "https://registry.npmjs.org/is-my-json-valid/-/is-my-json-valid-2.16.0.tgz", "resolved": "https://registry.npmjs.org/is-my-json-valid/-/is-my-json-valid-2.16.0.tgz",
"integrity": "sha1-8Hndm/2uZe4gOKrorLyGqxCeNpM=", "integrity": "sha1-8Hndm/2uZe4gOKrorLyGqxCeNpM=",
"dev": true "dev": true
}, },
"is-native": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/is-native/-/is-native-1.0.1.tgz",
"integrity": "sha1-zRjMFi6EUNaDtbq+eayZwUVElnU="
},
"is-nil": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/is-nil/-/is-nil-1.0.1.tgz",
"integrity": "sha1-LauingtYUGOHXntTnQcfWxWTeWk="
},
"is-npm": { "is-npm": {
"version": "1.0.0", "version": "1.0.0",
"resolved": "https://registry.npmjs.org/is-npm/-/is-npm-1.0.0.tgz", "resolved": "https://registry.npmjs.org/is-npm/-/is-npm-1.0.0.tgz",
@ -4865,6 +4915,11 @@
"resolved": "https://registry.npmjs.org/is-retry-allowed/-/is-retry-allowed-1.1.0.tgz", "resolved": "https://registry.npmjs.org/is-retry-allowed/-/is-retry-allowed-1.1.0.tgz",
"integrity": "sha1-EaBgVotnM5REAz0BJaYaINVk+zQ=" "integrity": "sha1-EaBgVotnM5REAz0BJaYaINVk+zQ="
}, },
"is-secret": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/is-secret/-/is-secret-1.1.1.tgz",
"integrity": "sha1-KYig6bOU41YM1IBAbWHKz9dPH/k="
},
"is-stream": { "is-stream": {
"version": "1.1.0", "version": "1.1.0",
"resolved": "https://registry.npmjs.org/is-stream/-/is-stream-1.1.0.tgz", "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-1.1.0.tgz",
@ -5265,6 +5320,11 @@
"resolved": "https://registry.npmjs.org/load-script/-/load-script-1.0.0.tgz", "resolved": "https://registry.npmjs.org/load-script/-/load-script-1.0.0.tgz",
"integrity": "sha1-BJGTngvuVkPuSUp+PaPSuscMbKQ=" "integrity": "sha1-BJGTngvuVkPuSUp+PaPSuscMbKQ="
}, },
"load-source-map": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/load-source-map/-/load-source-map-1.0.0.tgz",
"integrity": "sha1-MY9JkFzopwnft8w/FvPv47zx3QU="
},
"loader-utils": { "loader-utils": {
"version": "0.2.17", "version": "0.2.17",
"resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-0.2.17.tgz", "resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-0.2.17.tgz",
@ -6071,6 +6131,11 @@
"integrity": "sha1-mi3sg4Bvuy2XXyK+7IWcoms5OqE=", "integrity": "sha1-mi3sg4Bvuy2XXyK+7IWcoms5OqE=",
"dev": true "dev": true
}, },
"module-details-from-path": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/module-details-from-path/-/module-details-from-path-1.0.3.tgz",
"integrity": "sha1-EUyUlnPiqKNenTV4hSeqN7Z52is="
},
"module-not-found-error": { "module-not-found-error": {
"version": "1.0.1", "version": "1.0.1",
"resolved": "https://registry.npmjs.org/module-not-found-error/-/module-not-found-error-1.0.1.tgz", "resolved": "https://registry.npmjs.org/module-not-found-error/-/module-not-found-error-1.0.1.tgz",
@ -6441,6 +6506,11 @@
"integrity": "sha1-xkZdvwirzU2zWTF/eaxopkayj/k=", "integrity": "sha1-xkZdvwirzU2zWTF/eaxopkayj/k=",
"dev": true "dev": true
}, },
"normalize-bool": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/normalize-bool/-/normalize-bool-1.0.0.tgz",
"integrity": "sha1-RqVx7ZPqWrM3IfrM/FpZuGiQ2Fg="
},
"normalize-package-data": { "normalize-package-data": {
"version": "2.4.0", "version": "2.4.0",
"resolved": "https://registry.npmjs.org/normalize-package-data/-/normalize-package-data-2.4.0.tgz", "resolved": "https://registry.npmjs.org/normalize-package-data/-/normalize-package-data-2.4.0.tgz",
@ -6569,6 +6639,28 @@
"resolved": "https://registry.npmjs.org/onetime/-/onetime-1.1.0.tgz", "resolved": "https://registry.npmjs.org/onetime/-/onetime-1.1.0.tgz",
"integrity": "sha1-ofeDj4MUxRbwXs78vEzP4EtO14k=" "integrity": "sha1-ofeDj4MUxRbwXs78vEzP4EtO14k="
}, },
"opbeat": {
"version": "4.14.0",
"resolved": "https://registry.npmjs.org/opbeat/-/opbeat-4.14.0.tgz",
"integrity": "sha1-rpB3qvqRS3KkSAGQWjK8tT1+dd8=",
"dependencies": {
"end-of-stream": {
"version": "1.4.0",
"resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.0.tgz",
"integrity": "sha1-epDYM+/abPpurA9JSduw+tOmMgY="
}
}
},
"opbeat-http-client": {
"version": "1.2.2",
"resolved": "https://registry.npmjs.org/opbeat-http-client/-/opbeat-http-client-1.2.2.tgz",
"integrity": "sha1-itOZlp1QglTazi0IU5gTaBF9oz8="
},
"opbeat-release-tracker": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/opbeat-release-tracker/-/opbeat-release-tracker-1.1.1.tgz",
"integrity": "sha1-L2V2clC5Va6YjtyodazYNhIOfgo="
},
"open": { "open": {
"version": "0.0.5", "version": "0.0.5",
"resolved": "https://registry.npmjs.org/open/-/open-0.0.5.tgz", "resolved": "https://registry.npmjs.org/open/-/open-0.0.5.tgz",
@ -6613,7 +6705,8 @@
"options": { "options": {
"version": "0.0.6", "version": "0.0.6",
"resolved": "https://registry.npmjs.org/options/-/options-0.0.6.tgz", "resolved": "https://registry.npmjs.org/options/-/options-0.0.6.tgz",
"integrity": "sha1-7CLTEoBrtT5zF3Pnza788cZDEo8=" "integrity": "sha1-7CLTEoBrtT5zF3Pnza788cZDEo8=",
"dev": true
}, },
"orchestrator": { "orchestrator": {
"version": "0.3.8", "version": "0.3.8",
@ -6869,8 +6962,7 @@
"path-parse": { "path-parse": {
"version": "1.0.5", "version": "1.0.5",
"resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.5.tgz", "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.5.tgz",
"integrity": "sha1-PBrfhx6pzWyUMbbqK9dKD/BVxME=", "integrity": "sha1-PBrfhx6pzWyUMbbqK9dKD/BVxME="
"dev": true
}, },
"path-root": { "path-root": {
"version": "0.1.1", "version": "0.1.1",
@ -7364,6 +7456,11 @@
"integrity": "sha1-hSBLVNuoLVdC4oyWdW70OvUOM4Q=", "integrity": "sha1-hSBLVNuoLVdC4oyWdW70OvUOM4Q=",
"dev": true "dev": true
}, },
"redact-secrets": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/redact-secrets/-/redact-secrets-1.0.0.tgz",
"integrity": "sha1-YPHbVpJP6QogO6jMs5KDzbsNkHw="
},
"redent": { "redent": {
"version": "1.0.0", "version": "1.0.0",
"resolved": "https://registry.npmjs.org/redent/-/redent-1.0.0.tgz", "resolved": "https://registry.npmjs.org/redent/-/redent-1.0.0.tgz",
@ -7512,6 +7609,11 @@
"resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz",
"integrity": "sha1-jGStX9MNqxyXbiNE/+f3kqam30I=" "integrity": "sha1-jGStX9MNqxyXbiNE/+f3kqam30I="
}, },
"require-in-the-middle": {
"version": "2.1.2",
"resolved": "https://registry.npmjs.org/require-in-the-middle/-/require-in-the-middle-2.1.2.tgz",
"integrity": "sha1-vduJMW1FvNsI4sYYa9Lm6Bmo7q4="
},
"require-main-filename": { "require-main-filename": {
"version": "1.0.1", "version": "1.0.1",
"resolved": "https://registry.npmjs.org/require-main-filename/-/require-main-filename-1.0.1.tgz", "resolved": "https://registry.npmjs.org/require-main-filename/-/require-main-filename-1.0.1.tgz",
@ -7545,8 +7647,7 @@
"resolve": { "resolve": {
"version": "1.3.3", "version": "1.3.3",
"resolved": "https://registry.npmjs.org/resolve/-/resolve-1.3.3.tgz", "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.3.3.tgz",
"integrity": "sha1-ZVkHw0aahoDcLeOidaj91paR8OU=", "integrity": "sha1-ZVkHw0aahoDcLeOidaj91paR8OU="
"dev": true
}, },
"resolve-dir": { "resolve-dir": {
"version": "0.1.1", "version": "0.1.1",
@ -8259,6 +8360,11 @@
"resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz",
"integrity": "sha1-BOaSb2YolTVPPdAVIDYzuFcpfiw=" "integrity": "sha1-BOaSb2YolTVPPdAVIDYzuFcpfiw="
}, },
"sql-summary": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/sql-summary/-/sql-summary-1.0.0.tgz",
"integrity": "sha1-OeOlHY2F5Gc5g2/H1n0GVLFzo58="
},
"sse": { "sse": {
"version": "0.0.6", "version": "0.0.6",
"resolved": "https://registry.npmjs.org/sse/-/sse-0.0.6.tgz", "resolved": "https://registry.npmjs.org/sse/-/sse-0.0.6.tgz",
@ -8281,6 +8387,11 @@
"resolved": "https://registry.npmjs.org/stable/-/stable-0.1.6.tgz", "resolved": "https://registry.npmjs.org/stable/-/stable-0.1.6.tgz",
"integrity": "sha1-kQ9dKu17Ugxud3SZwfMuE5/eyxA=" "integrity": "sha1-kQ9dKu17Ugxud3SZwfMuE5/eyxA="
}, },
"stackman": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/stackman/-/stackman-2.0.1.tgz",
"integrity": "sha1-ztMJxmLpubZn79cYOxrjDFF8uqM="
},
"statuses": { "statuses": {
"version": "1.3.1", "version": "1.3.1",
"resolved": "https://registry.npmjs.org/statuses/-/statuses-1.3.1.tgz", "resolved": "https://registry.npmjs.org/statuses/-/statuses-1.3.1.tgz",
@ -8669,6 +8780,11 @@
"integrity": "sha1-TcGeZk38y+Jb2NtQiwDG2hWCVdE=", "integrity": "sha1-TcGeZk38y+Jb2NtQiwDG2hWCVdE=",
"dev": true "dev": true
}, },
"to-source-code": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/to-source-code/-/to-source-code-1.0.2.tgz",
"integrity": "sha1-3RNr2x4dvYC76s8IiZJnjpBwv+o="
},
"to-utf8": { "to-utf8": {
"version": "0.0.1", "version": "0.0.1",
"resolved": "https://registry.npmjs.org/to-utf8/-/to-utf8-0.0.1.tgz", "resolved": "https://registry.npmjs.org/to-utf8/-/to-utf8-0.0.1.tgz",
@ -8870,6 +8986,16 @@
"resolved": "https://registry.npmjs.org/underscore.string/-/underscore.string-3.3.4.tgz", "resolved": "https://registry.npmjs.org/underscore.string/-/underscore.string-3.3.4.tgz",
"integrity": "sha1-LCo/n4PmR2L9xF5s6sZRQoZCE9s=" "integrity": "sha1-LCo/n4PmR2L9xF5s6sZRQoZCE9s="
}, },
"unicode-byte-truncate": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/unicode-byte-truncate/-/unicode-byte-truncate-1.0.0.tgz",
"integrity": "sha1-qm8PNHUZP+IMMgrJIT425i6HZKc="
},
"unicode-substring": {
"version": "0.1.0",
"resolved": "https://registry.npmjs.org/unicode-substring/-/unicode-substring-0.1.0.tgz",
"integrity": "sha1-YSDOPDkDhdvND2DDK5BlxBgdSzY="
},
"unique-stream": { "unique-stream": {
"version": "1.0.0", "version": "1.0.0",
"resolved": "https://registry.npmjs.org/unique-stream/-/unique-stream-1.0.0.tgz", "resolved": "https://registry.npmjs.org/unique-stream/-/unique-stream-1.0.0.tgz",

View File

@ -89,6 +89,7 @@
"normalize-url": "^1.3.1", "normalize-url": "^1.3.1",
"normalizr": "2.2.1", "normalizr": "2.2.1",
"object.assign": "^4.0.3", "object.assign": "^4.0.3",
"opbeat": "^4.14.0",
"passport": "^0.2.1", "passport": "^0.2.1",
"passport-facebook": "^2.0.0", "passport-facebook": "^2.0.0",
"passport-github": "^1.0.0", "passport-github": "^1.0.0",

View File

@ -1,105 +0,0 @@
import { Observable } from 'rx';
import debugFactory from 'debug';
import { isEmail } from 'validator';
import path from 'path';
const debug = debugFactory('fcc:user:remote');
const isDev = process.env.NODE_ENV !== 'production';
const devHost = process.env.HOST || 'localhost';
function destroyAllRelated(id, Model) {
return Observable.fromNodeCallback(
Model.destroyAll,
Model
)({ userId: id });
}
module.exports = function(app) {
var User = app.models.User;
var UserIdentity = app.models.UserIdentity;
var UserCredential = app.models.UserCredential;
User.observe('before delete', function(ctx, next) {
debug('removing user', ctx.where);
var id = ctx.where && ctx.where.id ? ctx.where.id : null;
if (!id) {
return next();
}
return Observable.combineLatest(
destroyAllRelated(id, UserIdentity),
destroyAllRelated(id, UserCredential),
function(identData, credData) {
return {
identData: identData,
credData: credData
};
}
)
.subscribe(
function(data) {
debug('deleted', data);
},
function(err) {
debug('error deleting user %s stuff', id, err);
next(err);
},
function() {
debug('user stuff deleted for user %s', id);
next();
}
);
});
// set email varified false on user email signup
// should not be set with oauth signin methods
User.beforeRemote('create', function(ctx, user, next) {
var body = ctx.req.body;
if (body) {
// this is workaround for preventing a server crash
// refer strongloop/loopback/#1364
if (body.password === '') {
body.password = null;
}
body.emailVerified = false;
}
next();
});
// send welcome email to new camper
User.afterRemote('create', function({ req, res }, user, next) {
debug('user created, sending email');
if (!user.email || !isEmail(user.email)) { return next(); }
const redirect = req.session && req.session.returnTo ?
req.session.returnTo :
'/';
var mailOptions = {
type: 'email',
to: user.email,
from: 'Team@freecodecamp.com',
subject: 'Welcome to freeCodeCamp!',
protocol: isDev ? null : 'https',
host: isDev ? devHost : 'freecodecamp.com',
port: isDev ? null : 443,
template: path.join(
__dirname,
'..',
'views',
'emails',
'a-extend-user-welcome.ejs'
),
redirect: '/email-signin'
};
debug('sending welcome email');
return user.verify(mailOptions, function(err) {
if (err) { return next(err); }
req.flash('success', {
msg: [ 'Congratulations ! We\'ve created your account. ',
'Please check your email. We sent you a link that you can ',
'click to verify your email address and then login.'
].join('')
});
return res.redirect(redirect);
});
});
};

View File

@ -1,81 +0,0 @@
import { Observable } from 'rx';
import debugFactory from 'debug';
import dedent from 'dedent';
import { observeMethod, observeQuery } from '../utils/rx';
import { getSocialProvider } from '../utils/auth';
const debug = debugFactory('fcc:userIdent');
export default function({ models }) {
const { User, UserIdentity, UserCredential } = models;
const findUserById = observeMethod(User, 'findById');
const findIdent = observeMethod(UserIdentity, 'findOne');
UserIdentity.link = function(
userId,
provider,
authScheme,
profile,
credentials,
options = {},
cb
) {
if (typeof options === 'function' && !cb) {
cb = options;
options = {};
}
const user$ = findUserById(userId);
const query = {
where: {
provider: getSocialProvider(provider),
externalId: profile.id
}
};
debug('link identity query', query);
findIdent(query)
.flatMap(identity => {
const modified = new Date();
if (!identity) {
return observeQuery(UserIdentity, 'create', {
provider: getSocialProvider(provider),
externalId: profile.id,
authScheme,
profile,
credentials,
userId,
created: modified,
modified
});
}
if (identity.userId.toString() !== userId.toString()) {
return Observable.throw(
new Error(
dedent`
Your GitHub is already associated with another account.
You may have accidentally created a duplicate account.
No worries, though. We can fix this real quick.
Please email us with your GitHub username: team@freecodecamp.com.
`.split('/n').join(' ')
)
);
}
identity.credentials = credentials;
return observeQuery(identity, 'updateAttributes', {
profile,
credentials,
modified
});
})
.withLatestFrom(user$, (identity, user) => ({ identity, user }))
.subscribe(
({ identity, user }) => {
cb(null, user, identity);
},
cb
);
};
UserCredential.link = UserIdentity.link.bind(UserIdentity);
}

View File

@ -1,48 +1,10 @@
import passport from 'passport'; import passport from 'passport';
import { PassportConfigurator } from 'loopback-component-passport'; import { PassportConfigurator } from 'loopback-component-passport';
import passportProviders from './passport-providers'; import passportProviders from './passport-providers';
import uuid from 'uuid';
import { generateKey } from 'loopback-component-passport/lib/models/utils';
import {
setProfileFromGithub,
getSocialProvider,
getUsernameFromProvider
} from './utils/auth';
const passportOptions = { const passportOptions = {
emailOptional: true, emailOptional: true,
profileToUser(provider, profile) { profileToUser: null
const emails = profile.emails;
// NOTE(berks): get email or set to null.
// MongoDB indexs email but can be sparse(blank)
const email = emails && emails[0] && emails[0].value ?
emails[0].value :
null;
// create random username
// username will be assigned when camper signups for Github
const username = 'fcc' + uuid.v4().slice(0, 8);
const password = generateKey('password');
let userObj = {
username: username,
password: password
};
if (email) {
userObj.email = email;
}
if (!(/github/).test(provider)) {
userObj[getSocialProvider(provider)] = getUsernameFromProvider(
getSocialProvider(provider),
profile
);
} else {
userObj = setProfileFromGithub(userObj, profile, profile._json);
}
return userObj;
}
}; };
const fields = { const fields = {

View File

@ -8,6 +8,7 @@
"enableHttpContext": false "enableHttpContext": false
}, },
"rest": { "rest": {
"handleErrors": false,
"normalizeHttpPath": false, "normalizeHttpPath": false,
"xml": false "xml": false
}, },
@ -22,9 +23,6 @@
"cors": { "cors": {
"origin": true, "origin": true,
"credentials": true "credentials": true
},
"errorHandler": {
"disableStackTrace": false
} }
} }
} }

View File

@ -53,7 +53,7 @@
}, },
"files": {}, "files": {},
"final:after": { "final:after": {
"./middlewares/keymetrics": {}, "./middlewares/error-reporter": {},
"./middlewares/error-handlers": {} "./middlewares/error-handlers": {}
} }
} }

View File

@ -1,5 +1,6 @@
import errorHandler from 'errorhandler'; import errorHandler from 'errorhandler';
import accepts from 'accepts'; import accepts from 'accepts';
import { unwrapHandledError } from '../utils/create-handled-error.js';
export default function prodErrorHandler() { export default function prodErrorHandler() {
if (process.env.NODE_ENV === 'development') { if (process.env.NODE_ENV === 'development') {
@ -21,20 +22,24 @@ export default function prodErrorHandler() {
// parse res type // parse res type
const accept = accepts(req); const accept = accepts(req);
const type = accept.type('html', 'json', 'text'); const type = accept.type('html', 'json', 'text');
const handled = unwrapHandledError(err);
const message = 'Oops! Something went wrong. Please try again later'; const redirectTo = handled.redirectTo || '/map';
const message = handled.message ||
'Oops! Something went wrong. Please try again later';
if (type === 'html') { if (type === 'html') {
if (typeof req.flash === 'function') { if (typeof req.flash === 'function') {
req.flash(err.messageType || 'errors', { req.flash(
msg: err.userMessage || message handled.type || 'errors',
}); { msg: message }
);
} }
return res.redirect(err.redirectTo || '/map'); return res.redirect(redirectTo);
// json // json
} else if (type === 'json') { } else if (type === 'json') {
res.setHeader('Content-Type', 'application/json'); res.setHeader('Content-Type', 'application/json');
return res.send({ return res.send({
message: message message
}); });
// plain text // plain text
} else { } else {

View File

@ -0,0 +1,30 @@
import opbeat from 'opbeat';
import debug from 'debug';
import {
isHandledError,
unwrapHandledError
} from '../utils/create-handled-error.js';
const log = debug('fcc:middlewares:error-reporter');
export default function keymetrics() {
if (process.env.NODE_ENV !== 'production') {
return (err, req, res, next) => {
if (isHandledError(err)) {
// log out user messages in development
const handled = unwrapHandledError(err);
log(handled.message);
}
next(err);
};
}
return (err, req, res, next) => {
// handled errors do not need to be reported
// the report a message and redirect the user
if (isHandledError(err)) {
return next(err);
}
return opbeat.captureError(err, { request: req }, () => next(err));
};
}

View File

@ -1,20 +0,0 @@
import pmx from 'pmx';
export default function keymetrics() {
if (process.env.NODE_ENV !== 'production') {
return (err, req, res, next) => next(err);
}
return (err, req, res, next) => {
if (res.statusCode < 400) { res.statusCode = 500; }
err.url = req.url;
err.component = req.url;
err.action = req.method;
err.params = req.body;
err.session = req.session;
err.username = req.user ? req.user.username : 'anonymous';
err.userId = req.user ? req.user.id : 'anonymous';
return next(pmx.notify(err));
};
}

View File

@ -1,7 +1,7 @@
var successRedirect = '/'; const successRedirect = '/';
var failureRedirect = '/signin'; const failureRedirect = '/signin';
var linkFailureRedirect = '/account'; const linkSuccessRedirect = '/settings';
var githubProfileSuccessRedirect = '/settings'; const linkFailureRedirect = '/settings';
export default { export default {
local: { local: {
@ -36,7 +36,7 @@ export default {
authPath: '/link/facebook', authPath: '/link/facebook',
callbackURL: '/link/facebook/callback', callbackURL: '/link/facebook/callback',
callbackPath: '/link/facebook/callback', callbackPath: '/link/facebook/callback',
successRedirect: successRedirect, successRedirect: linkSuccessRedirect,
failureRedirect: linkFailureRedirect, failureRedirect: linkFailureRedirect,
scope: ['email', 'user_likes'], scope: ['email', 'user_likes'],
link: true, link: true,
@ -65,7 +65,7 @@ export default {
authPath: '/link/google', authPath: '/link/google',
callbackURL: '/link/google/callback', callbackURL: '/link/google/callback',
callbackPath: '/link/google/callback', callbackPath: '/link/google/callback',
successRedirect: successRedirect, successRedirect: linkSuccessRedirect,
failureRedirect: linkFailureRedirect, failureRedirect: linkFailureRedirect,
scope: ['email', 'profile'], scope: ['email', 'profile'],
link: true, link: true,
@ -91,7 +91,7 @@ export default {
authPath: '/link/twitter', authPath: '/link/twitter',
callbackURL: '/link/twitter/callback', callbackURL: '/link/twitter/callback',
callbackPath: '/link/twitter/callback', callbackPath: '/link/twitter/callback',
successRedirect: successRedirect, successRedirect: linkSuccessRedirect,
failureRedirect: linkFailureRedirect, failureRedirect: linkFailureRedirect,
consumerKey: process.env.TWITTER_KEY, consumerKey: process.env.TWITTER_KEY,
consumerSecret: process.env.TWITTER_SECRET, consumerSecret: process.env.TWITTER_SECRET,
@ -122,7 +122,7 @@ export default {
authPath: '/link/linkedin', authPath: '/link/linkedin',
callbackURL: '/link/linkedin/callback', callbackURL: '/link/linkedin/callback',
callbackPath: '/link/linkedin/callback', callbackPath: '/link/linkedin/callback',
successRedirect: successRedirect, successRedirect: linkSuccessRedirect,
failureRedirect: linkFailureRedirect, failureRedirect: linkFailureRedirect,
clientID: process.env.LINKEDIN_ID, clientID: process.env.LINKEDIN_ID,
clientSecret: process.env.LINKEDIN_SECRET, clientSecret: process.env.LINKEDIN_SECRET,
@ -153,7 +153,7 @@ export default {
authPath: '/link/github', authPath: '/link/github',
callbackURL: '/auth/github/callback/link', callbackURL: '/auth/github/callback/link',
callbackPath: '/auth/github/callback/link', callbackPath: '/auth/github/callback/link',
successRedirect: githubProfileSuccessRedirect, successRedirect: linkSuccessRedirect,
failureRedirect: linkFailureRedirect, failureRedirect: linkFailureRedirect,
clientID: process.env.GITHUB_ID, clientID: process.env.GITHUB_ID,
clientSecret: process.env.GITHUB_SECRET, clientSecret: process.env.GITHUB_SECRET,

View File

@ -1,7 +1,13 @@
require('dotenv').load(); require('dotenv').load();
var pmx = require('pmx');
pmx.init(); if (process.env.OPBEAT_ID) {
console.log('loading opbeat');
require('opbeat').start({
appId: process.env.OPBEAT_ID,
organizationId: process.env.OPBEAT_ORG_ID,
secretToken: process.env.OPBEAT_SECRET
});
}
var _ = require('lodash'), var _ = require('lodash'),
Rx = require('rx'), Rx = require('rx'),

View File

@ -1,5 +1,7 @@
const githubRegex = (/github/i);
const providerHash = { const providerHash = {
facebook: ({ id }) => id, facebook: ({ id }) => id,
github: ({ username }) => username,
twitter: ({ username }) => username, twitter: ({ username }) => username,
linkedin({ _json }) { linkedin({ _json }) {
return _json && _json.publicProfileUrl || null; return _json && _json.publicProfileUrl || null;
@ -13,48 +15,49 @@ export function getUsernameFromProvider(provider, profile) {
null; null;
} }
// using es6 argument destructing // createProfileAttributes(provider: String, profile: {}) => Object
export function setProfileFromGithub( export function createUserUpdatesFromProfile(provider, profile) {
user, if (githubRegex.test(provider)) {
{ return createProfileAttributesFromGithub(profile);
profileUrl: githubURL,
username
},
{
id: githubId,
avatar_url: picture,
email: githubEmail,
created_at: joinedGithubOn,
blog: website,
location,
bio,
name
} }
) { return {
return Object.assign( [getSocialProvider(provider)]: getUsernameFromProvider(
user, getSocialProvider(provider),
{ profile
name, )
email: user.email || githubEmail, };
username: username.toLowerCase(), }
// using es6 argument destructing
// createProfileAttributes(profile) => profileUpdate
function createProfileAttributesFromGithub(profile) {
const {
profileUrl: githubURL,
username,
_json: {
id: githubId,
avatar_url: picture,
email: githubEmail,
created_at: joinedGithubOn,
blog: website,
location, location,
bio, bio,
joinedGithubOn, name
website, } = {}
isGithubCool: true, } = profile;
picture, return {
githubId, name,
githubURL, username: username.toLowerCase(),
githubEmail, location,
githubProfile: githubURL bio,
} joinedGithubOn,
); website,
} isGithubCool: true,
picture,
export function getFirstImageFromProfile(profile) { githubId,
return profile && profile.photos && profile.photos[0] ? githubURL,
profile.photos[0].value : githubEmail,
null; githubProfile: githubURL
};
} }
export function getSocialProvider(provider) { export function getSocialProvider(provider) {

View File

@ -0,0 +1,18 @@
const _handledError = Symbol('handledError');
export function isHandledError(err) {
return !!err[_handledError];
}
export function unwrapHandledError(err) {
return err[_handledError] || {};
}
export function wrapHandledError(err, {
type,
message,
redirectTo
}) {
err[_handledError] = { type, message, redirectTo };
return err;
}