forked from freeCodeCamp/freeCodeCamp
-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
fix(signup): signup auth (freeCodeCamp#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
- Loading branch information
1 parent
cca28b5
commit ffe0911
Showing
17 changed files
with
590 additions
and
496 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | ||
); | ||
}; | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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(); | ||
}); | ||
}); | ||
} |
Oops, something went wrong.