Skip to content

Commit

Permalink
fix(signup): signup auth (freeCodeCamp#15628)
Browse files Browse the repository at this point in the history
* 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
  • Loading branch information
BerkeleyTrue authored and mhatout committed Feb 9, 2018
1 parent cca28b5 commit ffe0911
Show file tree
Hide file tree
Showing 17 changed files with 590 additions and 496 deletions.
88 changes: 88 additions & 0 deletions common/models/User-Credential.js
Original file line number Diff line number Diff line change
@@ -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
);
};
};
245 changes: 102 additions & 143 deletions common/models/User-Identity.js
Original file line number Diff line number Diff line change
@@ -1,172 +1,131 @@
import loopback from 'loopback';
import debugFactory from 'debug';
import { Observable } from 'rx';
// import debug from 'debug';
import dedent from 'dedent';

import {
setProfileFromGithub,
getFirstImageFromProfile,
getSocialProvider,
getUsernameFromProvider,
getSocialProvider
createUserUpdatesFromProfile
} 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 debug = debugFactory('fcc:models:userIdent');
// const log = debug('fcc:models:userIdent');

export default function(UserIdent) {
UserIdent.on('dataSourceAttached', () => {
UserIdent.findOne$ = observeMethod(UserIdent, 'findOne');
});
// original source
// github.com/strongloop/loopback-component-passport
const createAccountMessage =
'Accounts can only be created using GitHub or though email';
// find identity if it exist
// if not redirect to email signup
// if yes and github
// update profile
// update username
// update picture
UserIdent.login = function(
provider,
_provider,
authScheme,
profile,
credentials,
options,
cb
) {
const User = UserIdent.app.models.User;
const AccessToken = UserIdent.app.models.AccessToken;
const provider = getSocialProvider(_provider);
options = options || {};
if (typeof options === 'function' && !cb) {
cb = options;
options = {};
}
const userIdentityModel = UserIdent;
profile.id = profile.id || profile.openid;
const filter = {
const query = {
where: {
provider: getSocialProvider(provider),
provider: provider,
externalId: profile.id
}
},
include: 'user'
};
return userIdentityModel.findOne(filter)
.then(identity => {
// identity already exists
// find user and log them in
if (identity) {
identity.credentials = credentials;
const options = {
profile: profile,
credentials: credentials,
modified: new Date()
};
return identity.updateAttributes(options)
// grab user associated with identity
.then(() => identity.user())
.then(user => {
// Create access token for user
const options = {
created: new Date(),
ttl: user.constructor.settings.ttl
};
return user.accessTokens.create(options)
.then(token => ({ user, token }));
})
.then(({ token, user })=> {
cb(null, user, identity, token);
})
.catch(err => cb(err));
return UserIdent.findOne$(query)
.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'
}
);
}
// Find the user model
const userModel = userIdentityModel.relations.user &&
userIdentityModel.relations.user.modelTo ||
loopback.getModelByType(loopback.User);

const userObj = options.profileToUser(provider, profile, options);
if (getSocialProvider(provider) !== 'github') {
const err = new Error(createAccountMessage);
err.userMessage = createAccountMessage;
err.messageType = 'info';
err.redirectTo = '/signin';
return process.nextTick(() => cb(err));
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.
`
}
);
});
}

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));
});
const updateUser = User.update$(
{ id: user.id },
createUserUpdatesFromProfile(provider, profile)
).map(() => user);
// identity already exists
// find user and log them in
identity.credentials = credentials;
const attributes = {
// we no longer want to keep the profile
// this is information we do not need or use
profile: null,
credentials: credentials,
modified
};
const updateIdentity = observeQuery(
identity,
'updateAttributes',
attributes
);
const createToken = observeQuery(
AccessToken,
'create',
{
userId: user.id,
created: new Date(),
ttl: user.constructor.settings.ttl
}
);
return Observable.combineLatest(
updateUser,
updateIdentity,
createToken,
(user, identity, token) => ({ user, identity, token })
);
})
.subscribe(
({ user, identity, token }) => cb(null, user, identity, token),
cb
);
};

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();
});
});
}
Loading

0 comments on commit ffe0911

Please sign in to comment.