| 
									
										
										
										
											2018-06-12 16:50:35 +01:00
										 |  |  | /** | 
					
						
							|  |  |  |  * | 
					
						
							|  |  |  |  * Any ref to fixCompletedChallengesItem should be removed post | 
					
						
							|  |  |  |  * a db migration to fix all completedChallenges | 
					
						
							|  |  |  |  * | 
					
						
							|  |  |  |  */ | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2016-04-14 17:07:40 -07:00
										 |  |  | import { Observable } from 'rx'; | 
					
						
							| 
									
										
										
										
											2018-05-15 14:56:26 +01:00
										 |  |  | import uuid from 'uuid/v4'; | 
					
						
							| 
									
										
										
										
											2015-07-29 15:00:24 -07:00
										 |  |  | import moment from 'moment'; | 
					
						
							| 
									
										
										
										
											2015-12-28 12:41:37 -08:00
										 |  |  | import dedent from 'dedent'; | 
					
						
							| 
									
										
										
										
											2016-04-14 17:07:40 -07:00
										 |  |  | import debugFactory from 'debug'; | 
					
						
							| 
									
										
										
										
											2016-05-12 15:48:34 -07:00
										 |  |  | import { isEmail } from 'validator'; | 
					
						
							| 
									
										
										
										
											2016-05-07 17:46:39 +05:30
										 |  |  | import path from 'path'; | 
					
						
							| 
									
										
										
										
											2016-06-26 21:34:01 +05:30
										 |  |  | import loopback from 'loopback'; | 
					
						
							| 
									
										
										
										
											2018-02-16 23:18:53 +00:00
										 |  |  | import _ from 'lodash'; | 
					
						
							| 
									
										
										
										
											2018-05-23 21:10:56 +01:00
										 |  |  | import jwt from 'jsonwebtoken'; | 
					
						
							| 
									
										
										
										
											2018-08-03 01:28:49 +05:30
										 |  |  | import generate from 'nanoid/generate'; | 
					
						
							| 
									
										
										
										
											2016-04-14 17:07:40 -07:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2018-08-31 16:04:04 +01:00
										 |  |  | import { homeLocation, apiLocation } from '../../../config/env'; | 
					
						
							| 
									
										
										
										
											2018-08-29 20:52:41 +01:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2018-06-12 16:50:35 +01:00
										 |  |  | import { fixCompletedChallengeItem } from '../utils'; | 
					
						
							| 
									
										
										
										
											2017-12-07 16:13:19 -08:00
										 |  |  | import { themes } from '../utils/themes'; | 
					
						
							| 
									
										
										
										
											2017-07-13 11:39:07 -07:00
										 |  |  | import { saveUser, observeMethod } from '../../server/utils/rx.js'; | 
					
						
							|  |  |  | import { blacklistedUsernames } from '../../server/utils/constants.js'; | 
					
						
							|  |  |  | import { wrapHandledError } from '../../server/utils/create-handled-error.js'; | 
					
						
							| 
									
										
										
										
											2017-04-27 01:54:56 +05:30
										 |  |  | import { | 
					
						
							| 
									
										
										
										
											2018-02-16 23:18:53 +00:00
										 |  |  |   getEmailSender | 
					
						
							| 
									
										
										
										
											2017-04-27 01:54:56 +05:30
										 |  |  | } from '../../server/utils/url-utils.js'; | 
					
						
							| 
									
										
										
										
											2018-02-19 20:32:14 +00:00
										 |  |  | import { | 
					
						
							|  |  |  |   normaliseUserFields, | 
					
						
							|  |  |  |   getProgress, | 
					
						
							|  |  |  |   publicUserProps | 
					
						
							|  |  |  | } from '../../server/utils/publicUserProps'; | 
					
						
							| 
									
										
										
										
											2015-07-29 15:00:24 -07:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2018-06-12 16:50:35 +01:00
										 |  |  | const log = debugFactory('fcc:models:user'); | 
					
						
							| 
									
										
										
										
											2015-08-15 12:19:36 -07:00
										 |  |  | const BROWNIEPOINTS_TIMEOUT = [1, 'hour']; | 
					
						
							| 
									
										
										
										
											2018-08-03 01:28:49 +05:30
										 |  |  | const nanoidCharSet = | 
					
						
							|  |  |  |   '0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ'; | 
					
						
							| 
									
										
										
										
											2015-07-29 15:00:24 -07:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2017-12-27 10:52:13 -08:00
										 |  |  | const createEmailError = redirectTo => wrapHandledError( | 
					
						
							|  |  |  |   new Error('email format is invalid'), | 
					
						
							|  |  |  |   { | 
					
						
							|  |  |  |     type: 'info', | 
					
						
							|  |  |  |     message: 'Please check to make sure the email is a valid email address.', | 
					
						
							|  |  |  |     redirectTo | 
					
						
							|  |  |  |   } | 
					
						
							| 
									
										
										
										
											2017-07-13 11:39:07 -07:00
										 |  |  | ); | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | function destroyAll(id, Model) { | 
					
						
							|  |  |  |   return Observable.fromNodeCallback( | 
					
						
							|  |  |  |     Model.destroyAll, | 
					
						
							|  |  |  |     Model | 
					
						
							|  |  |  |   )({ userId: id }); | 
					
						
							|  |  |  | } | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2018-05-15 14:56:26 +01:00
										 |  |  | function buildCompletedChallengesUpdate(completedChallenges, project) { | 
					
						
							| 
									
										
										
										
											2018-03-05 14:15:30 +00:00
										 |  |  |   const key = Object.keys(project)[0]; | 
					
						
							|  |  |  |   const solutions = project[key]; | 
					
						
							| 
									
										
										
										
											2018-05-24 14:59:46 +01:00
										 |  |  |   const solutionKeys = Object.keys(solutions); | 
					
						
							| 
									
										
										
										
											2018-06-12 16:50:35 +01:00
										 |  |  |   const currentCompletedChallenges = [ | 
					
						
							|  |  |  |     ...completedChallenges.map(fixCompletedChallengeItem) | 
					
						
							|  |  |  |   ]; | 
					
						
							| 
									
										
										
										
											2018-05-15 14:56:26 +01:00
										 |  |  |   const currentCompletedProjects = currentCompletedChallenges | 
					
						
							| 
									
										
										
										
											2018-05-24 14:59:46 +01:00
										 |  |  |     .filter(({id}) => solutionKeys.includes(id)); | 
					
						
							| 
									
										
										
										
											2018-02-16 23:18:53 +00:00
										 |  |  |   const now = Date.now(); | 
					
						
							| 
									
										
										
										
											2018-05-24 14:59:46 +01:00
										 |  |  |   const update = solutionKeys.reduce((update, currentId) => { | 
					
						
							| 
									
										
										
										
											2018-05-15 14:56:26 +01:00
										 |  |  |     const indexOfCurrentId = _.findIndex( | 
					
						
							| 
									
										
										
										
											2018-05-24 14:59:46 +01:00
										 |  |  |       update, | 
					
						
							| 
									
										
										
										
											2018-05-15 14:56:26 +01:00
										 |  |  |       ({id}) => id === currentId | 
					
						
							|  |  |  |     ); | 
					
						
							|  |  |  |     const isCurrentlyCompleted = indexOfCurrentId !== -1; | 
					
						
							| 
									
										
										
										
											2018-05-24 14:59:46 +01:00
										 |  |  |     if (isCurrentlyCompleted) { | 
					
						
							| 
									
										
										
										
											2018-05-15 14:56:26 +01:00
										 |  |  |       update[indexOfCurrentId] = { | 
					
						
							| 
									
										
										
										
											2018-06-12 16:50:35 +01:00
										 |  |  |         ..._.find(update, ({id}) => id === currentId), | 
					
						
							| 
									
										
										
										
											2018-05-15 14:56:26 +01:00
										 |  |  |         solution: solutions[currentId] | 
					
						
							| 
									
										
										
										
											2018-02-16 23:18:53 +00:00
										 |  |  |       }; | 
					
						
							|  |  |  |     } | 
					
						
							| 
									
										
										
										
											2018-05-15 14:56:26 +01:00
										 |  |  |     if (!isCurrentlyCompleted) { | 
					
						
							|  |  |  |       return [ | 
					
						
							| 
									
										
										
										
											2018-02-16 23:18:53 +00:00
										 |  |  |         ...update, | 
					
						
							| 
									
										
										
										
											2018-05-15 14:56:26 +01:00
										 |  |  |         { | 
					
						
							| 
									
										
										
										
											2018-02-16 23:18:53 +00:00
										 |  |  |           id: currentId, | 
					
						
							| 
									
										
										
										
											2018-03-05 14:15:30 +00:00
										 |  |  |           solution: solutions[currentId], | 
					
						
							| 
									
										
										
										
											2018-02-16 23:18:53 +00:00
										 |  |  |           challengeType: 3, | 
					
						
							| 
									
										
										
										
											2018-05-15 14:56:26 +01:00
										 |  |  |           completedDate: now | 
					
						
							| 
									
										
										
										
											2018-02-16 23:18:53 +00:00
										 |  |  |         } | 
					
						
							| 
									
										
										
										
											2018-05-15 14:56:26 +01:00
										 |  |  |       ]; | 
					
						
							| 
									
										
										
										
											2018-02-16 23:18:53 +00:00
										 |  |  |     } | 
					
						
							|  |  |  |     return update; | 
					
						
							| 
									
										
										
										
											2018-05-15 14:56:26 +01:00
										 |  |  |   }, currentCompletedProjects); | 
					
						
							|  |  |  |   const updatedExisting = _.uniqBy( | 
					
						
							|  |  |  |     [ | 
					
						
							|  |  |  |       ...update, | 
					
						
							|  |  |  |       ...currentCompletedChallenges | 
					
						
							|  |  |  |     ], | 
					
						
							|  |  |  |     'id' | 
					
						
							|  |  |  |   ); | 
					
						
							| 
									
										
										
										
											2018-05-24 14:59:46 +01:00
										 |  |  |   return { | 
					
						
							|  |  |  |     updated: updatedExisting, | 
					
						
							|  |  |  |     isNewCompletionCount: | 
					
						
							|  |  |  |       updatedExisting.length - completedChallenges.length | 
					
						
							|  |  |  |   }; | 
					
						
							| 
									
										
										
										
											2018-02-16 23:18:53 +00:00
										 |  |  | } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | function isTheSame(val1, val2) { | 
					
						
							|  |  |  |   return val1 === val2; | 
					
						
							|  |  |  | } | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2016-11-23 19:54:08 +05:30
										 |  |  | const renderSignUpEmail = loopback.template(path.join( | 
					
						
							|  |  |  |   __dirname, | 
					
						
							|  |  |  |   '..', | 
					
						
							|  |  |  |   '..', | 
					
						
							|  |  |  |   'server', | 
					
						
							|  |  |  |   'views', | 
					
						
							|  |  |  |   'emails', | 
					
						
							|  |  |  |   'user-request-sign-up.ejs' | 
					
						
							|  |  |  | )); | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | const renderSignInEmail = loopback.template(path.join( | 
					
						
							|  |  |  |   __dirname, | 
					
						
							|  |  |  |   '..', | 
					
						
							|  |  |  |   '..', | 
					
						
							|  |  |  |   'server', | 
					
						
							|  |  |  |   'views', | 
					
						
							|  |  |  |   'emails', | 
					
						
							|  |  |  |   'user-request-sign-in.ejs' | 
					
						
							|  |  |  | )); | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2018-02-16 23:18:53 +00:00
										 |  |  | const renderEmailChangeEmail = loopback.template(path.join( | 
					
						
							|  |  |  |   __dirname, | 
					
						
							|  |  |  |   '..', | 
					
						
							|  |  |  |   '..', | 
					
						
							|  |  |  |   'server', | 
					
						
							|  |  |  |   'views', | 
					
						
							|  |  |  |   'emails', | 
					
						
							|  |  |  |   'user-request-update-email.ejs' | 
					
						
							|  |  |  | )); | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2015-07-29 15:00:24 -07:00
										 |  |  | function getAboutProfile({ | 
					
						
							|  |  |  |   username, | 
					
						
							| 
									
										
										
										
											2016-04-14 17:07:40 -07:00
										 |  |  |   githubProfile: github, | 
					
						
							|  |  |  |   progressTimestamps = [], | 
					
						
							|  |  |  |   bio | 
					
						
							| 
									
										
										
										
											2015-07-29 15:00:24 -07:00
										 |  |  | }) { | 
					
						
							|  |  |  |   return { | 
					
						
							|  |  |  |     username, | 
					
						
							| 
									
										
										
										
											2016-04-14 17:07:40 -07:00
										 |  |  |     github, | 
					
						
							|  |  |  |     browniePoints: progressTimestamps.length, | 
					
						
							|  |  |  |     bio | 
					
						
							| 
									
										
										
										
											2015-07-29 15:00:24 -07:00
										 |  |  |   }; | 
					
						
							|  |  |  | } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | function nextTick(fn) { | 
					
						
							|  |  |  |   return process.nextTick(fn); | 
					
						
							|  |  |  | } | 
					
						
							| 
									
										
										
										
											2015-06-11 19:11:07 -04:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2016-10-26 13:22:33 +00:00
										 |  |  | function getWaitPeriod(ttl) { | 
					
						
							|  |  |  |   const fiveMinutesAgo = moment().subtract(5, 'minutes'); | 
					
						
							|  |  |  |   const lastEmailSentAt = moment(new Date(ttl || null)); | 
					
						
							|  |  |  |   const isWaitPeriodOver = ttl ? | 
					
						
							|  |  |  |     lastEmailSentAt.isBefore(fiveMinutesAgo) : true; | 
					
						
							| 
									
										
										
										
											2017-12-27 11:16:53 -08:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2016-10-26 13:22:33 +00:00
										 |  |  |   if (!isWaitPeriodOver) { | 
					
						
							|  |  |  |     const minutesLeft = 5 - | 
					
						
							|  |  |  |       (moment().minutes() - lastEmailSentAt.minutes()); | 
					
						
							|  |  |  |     return minutesLeft; | 
					
						
							|  |  |  |   } | 
					
						
							| 
									
										
										
										
											2017-12-27 11:16:53 -08:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2016-10-26 13:22:33 +00:00
										 |  |  |   return 0; | 
					
						
							|  |  |  | } | 
					
						
							| 
									
										
										
										
											2017-12-27 11:16:53 -08:00
										 |  |  | 
 | 
					
						
							|  |  |  | function getWaitMessage(ttl) { | 
					
						
							|  |  |  |   const minutesLeft = getWaitPeriod(ttl); | 
					
						
							|  |  |  |   if (minutesLeft <= 0) { | 
					
						
							|  |  |  |     return null; | 
					
						
							|  |  |  |   } | 
					
						
							|  |  |  |   const timeToWait = minutesLeft ? | 
					
						
							|  |  |  |     `${minutesLeft} minute${minutesLeft > 1 ? 's' : ''}` : | 
					
						
							|  |  |  |     'a few seconds'; | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |   return dedent`
 | 
					
						
							|  |  |  |     Please wait ${timeToWait} to resend an authentication link. | 
					
						
							|  |  |  |   `;
 | 
					
						
							|  |  |  | } | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2015-06-11 19:11:07 -04:00
										 |  |  | module.exports = function(User) { | 
					
						
							| 
									
										
										
										
											2015-06-12 11:38:00 -07:00
										 |  |  |   // set salt factor for passwords
 | 
					
						
							|  |  |  |   User.settings.saltWorkFactor = 5; | 
					
						
							| 
									
										
										
										
											2015-08-15 12:19:36 -07:00
										 |  |  |   // set user.rand to random number
 | 
					
						
							| 
									
										
										
										
											2015-08-15 12:28:19 -07:00
										 |  |  |   User.definition.rawProperties.rand.default = | 
					
						
							|  |  |  |     User.definition.properties.rand.default = function() { | 
					
						
							| 
									
										
										
										
											2015-08-15 12:19:36 -07:00
										 |  |  |       return Math.random(); | 
					
						
							|  |  |  |     }; | 
					
						
							| 
									
										
										
										
											2016-04-21 20:35:19 -07:00
										 |  |  |   // increase user accessToken ttl to 900 days
 | 
					
						
							|  |  |  |   User.settings.ttl = 900 * 24 * 60 * 60 * 1000; | 
					
						
							| 
									
										
										
										
											2015-06-12 13:54:38 -07:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2015-06-12 16:23:20 -07:00
										 |  |  |   // username should not be in blacklist
 | 
					
						
							|  |  |  |   User.validatesExclusionOf('username', { | 
					
						
							| 
									
										
										
										
											2016-04-15 23:48:02 +08:00
										 |  |  |     in: blacklistedUsernames, | 
					
						
							| 
									
										
										
										
											2015-06-12 16:23:20 -07:00
										 |  |  |     message: 'is taken' | 
					
						
							|  |  |  |   }); | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |   // username should be unique
 | 
					
						
							|  |  |  |   User.validatesUniquenessOf('username'); | 
					
						
							| 
									
										
										
										
											2015-08-17 23:57:38 -07:00
										 |  |  |   User.settings.emailVerificationRequired = false; | 
					
						
							| 
									
										
										
										
											2015-06-12 13:54:38 -07:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2016-02-09 14:33:25 -08:00
										 |  |  |   User.on('dataSourceAttached', () => { | 
					
						
							|  |  |  |     User.findOne$ = Observable.fromNodeCallback(User.findOne, User); | 
					
						
							|  |  |  |     User.update$ = Observable.fromNodeCallback(User.updateAll, User); | 
					
						
							|  |  |  |     User.count$ = Observable.fromNodeCallback(User.count, User); | 
					
						
							| 
									
										
										
										
											2017-12-26 20:12:15 -08:00
										 |  |  |     User.create$ = Observable.fromNodeCallback( | 
					
						
							|  |  |  |       User.create.bind(User) | 
					
						
							|  |  |  |     ); | 
					
						
							| 
									
										
										
										
											2017-04-24 00:37:10 +05:30
										 |  |  |     User.prototype.createAccessToken$ = Observable.fromNodeCallback( | 
					
						
							|  |  |  |       User.prototype.createAccessToken | 
					
						
							|  |  |  |     ); | 
					
						
							| 
									
										
										
										
											2016-02-09 14:33:25 -08:00
										 |  |  |   }); | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2017-12-28 20:37:10 -08:00
										 |  |  |   User.observe('before save', function(ctx) { | 
					
						
							|  |  |  |     const beforeCreate = Observable.of(ctx) | 
					
						
							|  |  |  |       .filter(({ isNewInstance }) => isNewInstance) | 
					
						
							|  |  |  |       // User.create
 | 
					
						
							|  |  |  |       .map(({ instance }) => instance) | 
					
						
							|  |  |  |       .flatMap(user => { | 
					
						
							|  |  |  |         // note(berks): we now require all new users to supply an email
 | 
					
						
							|  |  |  |         // this was not always the case
 | 
					
						
							|  |  |  |         if ( | 
					
						
							|  |  |  |           typeof user.email !== 'string' || | 
					
						
							|  |  |  |           !isEmail(user.email) | 
					
						
							|  |  |  |         ) { | 
					
						
							|  |  |  |           throw createEmailError(); | 
					
						
							|  |  |  |         } | 
					
						
							|  |  |  |         // assign random username to new users
 | 
					
						
							|  |  |  |         // actual usernames will come from github
 | 
					
						
							|  |  |  |         // use full uuid to ensure uniqueness
 | 
					
						
							| 
									
										
										
										
											2018-05-15 14:56:26 +01:00
										 |  |  |         user.username = 'fcc' + uuid(); | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |         if (!user.externalId) { | 
					
						
							|  |  |  |           user.externalId = uuid(); | 
					
						
							|  |  |  |         } | 
					
						
							|  |  |  |         if (!user.unsubscribeId) { | 
					
						
							| 
									
										
										
										
											2018-08-03 01:28:49 +05:30
										 |  |  |           user.unsubscribeId = generate(nanoidCharSet, 20); | 
					
						
							| 
									
										
										
										
											2018-05-15 14:56:26 +01:00
										 |  |  |         } | 
					
						
							| 
									
										
										
										
											2017-12-28 20:37:10 -08:00
										 |  |  | 
 | 
					
						
							|  |  |  |         if (!user.progressTimestamps) { | 
					
						
							|  |  |  |           user.progressTimestamps = []; | 
					
						
							|  |  |  |         } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |         if (user.progressTimestamps.length === 0) { | 
					
						
							| 
									
										
										
										
											2018-07-01 10:28:46 +01:00
										 |  |  |           user.progressTimestamps.push(Date.now()); | 
					
						
							| 
									
										
										
										
											2017-12-28 20:37:10 -08:00
										 |  |  |         } | 
					
						
							|  |  |  |         return Observable.fromPromise(User.doesExist(null, user.email)) | 
					
						
							|  |  |  |           .do(exists => { | 
					
						
							|  |  |  |             if (exists) { | 
					
						
							|  |  |  |               throw wrapHandledError( | 
					
						
							|  |  |  |                 new Error('user already exists'), | 
					
						
							|  |  |  |                 { | 
					
						
							| 
									
										
										
										
											2018-08-29 20:52:41 +01:00
										 |  |  |                   redirectTo: `${homeLocation}/signin`, | 
					
						
							| 
									
										
										
										
											2017-12-28 20:37:10 -08:00
										 |  |  |                   message: dedent`
 | 
					
						
							|  |  |  |         The ${user.email} email address is already associated with an account. | 
					
						
							|  |  |  |         Try signing in with it here instead. | 
					
						
							|  |  |  |                   `
 | 
					
						
							|  |  |  |                 } | 
					
						
							|  |  |  |               ); | 
					
						
							|  |  |  |             } | 
					
						
							|  |  |  |           }); | 
					
						
							| 
									
										
										
										
											2017-07-13 11:39:07 -07:00
										 |  |  |       }) | 
					
						
							| 
									
										
										
										
											2017-12-28 20:37:10 -08:00
										 |  |  |       .ignoreElements(); | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     const updateOrSave = Observable.of(ctx) | 
					
						
							|  |  |  |       // not new
 | 
					
						
							|  |  |  |       .filter(({ isNewInstance }) => !isNewInstance) | 
					
						
							|  |  |  |       .map(({ instance }) => instance) | 
					
						
							|  |  |  |       // is update or save user
 | 
					
						
							|  |  |  |       .filter(Boolean) | 
					
						
							|  |  |  |       .do(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)) { | 
					
						
							|  |  |  |           throw createEmailError(); | 
					
						
							| 
									
										
										
										
											2017-07-13 11:39:07 -07:00
										 |  |  |         } | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2017-12-28 20:37:10 -08:00
										 |  |  |         user.username = user.username.trim().toLowerCase(); | 
					
						
							|  |  |  |         user.email = typeof user.email === 'string' ? | 
					
						
							|  |  |  |           user.email.trim().toLowerCase() : | 
					
						
							|  |  |  |           user.email; | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |         if (!user.progressTimestamps) { | 
					
						
							|  |  |  |           user.progressTimestamps = []; | 
					
						
							|  |  |  |         } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |         if (user.progressTimestamps.length === 0) { | 
					
						
							| 
									
										
										
										
											2018-05-15 14:56:26 +01:00
										 |  |  |           user.progressTimestamps.push(Date.now()); | 
					
						
							|  |  |  |         } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |         if (!user.externalId) { | 
					
						
							|  |  |  |           user.externalId = uuid(); | 
					
						
							|  |  |  |         } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |         if (!user.unsubscribeId) { | 
					
						
							| 
									
										
										
										
											2018-08-03 01:28:49 +05:30
										 |  |  |           user.unsubscribeId = generate(nanoidCharSet, 20); | 
					
						
							| 
									
										
										
										
											2017-12-28 20:37:10 -08:00
										 |  |  |         } | 
					
						
							|  |  |  |       }) | 
					
						
							|  |  |  |       .ignoreElements(); | 
					
						
							|  |  |  |     return Observable.merge(beforeCreate, updateOrSave) | 
					
						
							|  |  |  |       .toPromise(); | 
					
						
							| 
									
										
										
										
											2015-08-05 18:51:15 -07:00
										 |  |  |   }); | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2017-07-13 11:39:07 -07:00
										 |  |  |   // 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; | 
					
						
							| 
									
										
										
										
											2018-06-12 16:50:35 +01:00
										 |  |  |     log('removing user', ctx.where); | 
					
						
							| 
									
										
										
										
											2017-07-13 11:39:07 -07:00
										 |  |  |     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) { | 
					
						
							| 
									
										
										
										
											2018-06-12 16:50:35 +01:00
										 |  |  |           log('deleted', data); | 
					
						
							| 
									
										
										
										
											2017-07-13 11:39:07 -07:00
										 |  |  |         }, | 
					
						
							|  |  |  |         function(err) { | 
					
						
							| 
									
										
										
										
											2018-06-12 16:50:35 +01:00
										 |  |  |           log('error deleting user %s stuff', id, err); | 
					
						
							| 
									
										
										
										
											2017-07-13 11:39:07 -07:00
										 |  |  |           next(err); | 
					
						
							|  |  |  |         }, | 
					
						
							|  |  |  |         function() { | 
					
						
							| 
									
										
										
										
											2018-06-12 16:50:35 +01:00
										 |  |  |           log('user stuff deleted for user %s', id); | 
					
						
							| 
									
										
										
										
											2017-07-13 11:39:07 -07:00
										 |  |  |           next(); | 
					
						
							|  |  |  |         } | 
					
						
							|  |  |  |       ); | 
					
						
							|  |  |  |   }); | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2018-06-12 16:50:35 +01:00
										 |  |  |   log('setting up user hooks'); | 
					
						
							| 
									
										
										
										
											2017-12-29 10:49:49 -08:00
										 |  |  |   // overwrite lb confirm
 | 
					
						
							|  |  |  |   User.confirm = function(uid, token, redirectTo) { | 
					
						
							|  |  |  |     return this.findById(uid) | 
					
						
							|  |  |  |       .then(user => { | 
					
						
							|  |  |  |         if (!user) { | 
					
						
							|  |  |  |           throw wrapHandledError( | 
					
						
							|  |  |  |             new Error(`User not found: ${uid}`), | 
					
						
							| 
									
										
										
										
											2017-12-27 10:11:17 -08:00
										 |  |  |             { | 
					
						
							| 
									
										
										
										
											2017-12-29 10:49:49 -08:00
										 |  |  |               // standard oops
 | 
					
						
							|  |  |  |               type: 'info', | 
					
						
							|  |  |  |               redirectTo | 
					
						
							| 
									
										
										
										
											2017-12-27 10:11:17 -08:00
										 |  |  |             } | 
					
						
							|  |  |  |           ); | 
					
						
							| 
									
										
										
										
											2016-06-23 11:11:56 +05:30
										 |  |  |         } | 
					
						
							| 
									
										
										
										
											2017-12-29 10:49:49 -08:00
										 |  |  |         if (user.verificationToken !== token) { | 
					
						
							|  |  |  |           throw wrapHandledError( | 
					
						
							|  |  |  |             new Error(`Invalid token: ${token}`), | 
					
						
							|  |  |  |             { | 
					
						
							|  |  |  |               type: 'info', | 
					
						
							|  |  |  |               message: dedent`
 | 
					
						
							|  |  |  |                 Looks like you have clicked an invalid link. | 
					
						
							|  |  |  |                 Please sign in and request a fresh one. | 
					
						
							|  |  |  |               `,
 | 
					
						
							|  |  |  |               redirectTo | 
					
						
							|  |  |  |             } | 
					
						
							|  |  |  |           ); | 
					
						
							| 
									
										
										
										
											2016-06-23 11:11:56 +05:30
										 |  |  |         } | 
					
						
							| 
									
										
										
										
											2017-10-28 00:24:00 +05:30
										 |  |  |         return user.update$({ | 
					
						
							|  |  |  |           email: user.newEmail, | 
					
						
							| 
									
										
										
										
											2017-12-29 10:49:49 -08:00
										 |  |  |           emailVerified: true, | 
					
						
							|  |  |  |           emailVerifyTTL: null, | 
					
						
							| 
									
										
										
										
											2017-10-28 00:24:00 +05:30
										 |  |  |           newEmail: null, | 
					
						
							| 
									
										
										
										
											2017-12-29 10:49:49 -08:00
										 |  |  |           verificationToken: null | 
					
						
							|  |  |  |         }).toPromise(); | 
					
						
							|  |  |  |       }); | 
					
						
							|  |  |  |   }; | 
					
						
							| 
									
										
										
										
											2015-08-25 22:27:01 -07:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2018-06-12 16:50:35 +01:00
										 |  |  |   function manualReload() { | 
					
						
							|  |  |  |     this.reload((err, instance) => { | 
					
						
							|  |  |  |       if (err) { | 
					
						
							|  |  |  |         throw Error('failed to reload user instance'); | 
					
						
							|  |  |  |       } | 
					
						
							|  |  |  |       Object.assign(this, instance); | 
					
						
							|  |  |  |       log('user reloaded from db'); | 
					
						
							|  |  |  |     }); | 
					
						
							|  |  |  |   } | 
					
						
							|  |  |  |   User.prototype.manualReload = manualReload; | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2018-02-16 23:18:53 +00:00
										 |  |  |   User.prototype.loginByRequest = function loginByRequest(req, res) { | 
					
						
							|  |  |  |     const { | 
					
						
							|  |  |  |       query: { | 
					
						
							|  |  |  |         emailChange | 
					
						
							|  |  |  |       } | 
					
						
							|  |  |  |     } = req; | 
					
						
							| 
									
										
										
										
											2017-12-29 11:28:42 -08:00
										 |  |  |     const createToken = this.createAccessToken$() | 
					
						
							|  |  |  |       .do(accessToken => { | 
					
						
							|  |  |  |         const config = { | 
					
						
							|  |  |  |           signed: !!req.signedCookies, | 
					
						
							| 
									
										
										
										
											2018-05-21 21:26:18 +05:30
										 |  |  |           maxAge: accessToken.ttl, | 
					
						
							| 
									
										
										
										
											2018-05-22 21:53:09 +05:30
										 |  |  |           domain: process.env.COOKIE_DOMAIN || 'localhost' | 
					
						
							| 
									
										
										
										
											2017-12-29 11:28:42 -08:00
										 |  |  |         }; | 
					
						
							|  |  |  |         if (accessToken && accessToken.id) { | 
					
						
							| 
									
										
										
										
											2018-05-23 21:10:56 +01:00
										 |  |  |           const jwtAccess = jwt.sign({accessToken}, process.env.JWT_SECRET); | 
					
						
							|  |  |  |           res.cookie('jwt_access_token', jwtAccess, config); | 
					
						
							| 
									
										
										
										
											2017-12-29 11:28:42 -08:00
										 |  |  |           res.cookie('access_token', accessToken.id, config); | 
					
						
							|  |  |  |           res.cookie('userId', accessToken.userId, config); | 
					
						
							| 
									
										
										
										
											2016-01-17 11:15:05 +04:00
										 |  |  |         } | 
					
						
							| 
									
										
										
										
											2017-12-29 11:28:42 -08:00
										 |  |  |       }); | 
					
						
							| 
									
										
										
										
											2018-02-16 23:18:53 +00:00
										 |  |  |     let data = { | 
					
						
							| 
									
										
										
										
											2017-12-29 11:28:42 -08:00
										 |  |  |       emailVerified: true, | 
					
						
							|  |  |  |       emailAuthLinkTTL: null, | 
					
						
							|  |  |  |       emailVerifyTTL: null | 
					
						
							| 
									
										
										
										
											2018-02-16 23:18:53 +00:00
										 |  |  |     }; | 
					
						
							|  |  |  |     if (emailChange && this.newEmail) { | 
					
						
							|  |  |  |       data = { | 
					
						
							|  |  |  |         ...data, | 
					
						
							|  |  |  |         email: this.newEmail, | 
					
						
							|  |  |  |         newEmail: null | 
					
						
							|  |  |  |       }; | 
					
						
							|  |  |  |     } | 
					
						
							|  |  |  |     const updateUser = this.update$(data); | 
					
						
							| 
									
										
										
										
											2017-12-29 11:28:42 -08:00
										 |  |  |     return Observable.combineLatest( | 
					
						
							|  |  |  |       createToken, | 
					
						
							|  |  |  |       updateUser, | 
					
						
							|  |  |  |       req.logIn(this), | 
					
						
							|  |  |  |       (accessToken) => accessToken, | 
					
						
							|  |  |  |     ); | 
					
						
							|  |  |  |   }; | 
					
						
							| 
									
										
										
										
											2015-06-16 00:27:32 -04:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2018-05-26 18:28:20 +05:30
										 |  |  |   User.afterRemote('logout', function({req, res}, result, next) { | 
					
						
							|  |  |  |     const config = { | 
					
						
							|  |  |  |       signed: !!req.signedCookies, | 
					
						
							|  |  |  |       domain: process.env.COOKIE_DOMAIN || 'localhost' | 
					
						
							|  |  |  |     }; | 
					
						
							|  |  |  |     res.clearCookie('jwt_access_token', config); | 
					
						
							|  |  |  |     res.clearCookie('access_token', config); | 
					
						
							|  |  |  |     res.clearCookie('userId', config); | 
					
						
							|  |  |  |     res.clearCookie('_csrf', config); | 
					
						
							| 
									
										
										
										
											2015-06-11 19:11:07 -04:00
										 |  |  |     next(); | 
					
						
							|  |  |  |   }); | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2015-12-28 13:52:28 -08:00
										 |  |  |   User.doesExist = function doesExist(username, email) { | 
					
						
							| 
									
										
										
										
											2016-05-12 15:48:34 -07:00
										 |  |  |     if (!username && (!email || !isEmail(email))) { | 
					
						
							| 
									
										
										
										
											2015-12-28 13:52:28 -08:00
										 |  |  |       return Promise.resolve(false); | 
					
						
							| 
									
										
										
										
											2015-06-11 19:11:07 -04:00
										 |  |  |     } | 
					
						
							| 
									
										
										
										
											2018-06-12 16:50:35 +01:00
										 |  |  |     log('checking existence'); | 
					
						
							| 
									
										
										
										
											2015-06-11 16:46:31 -07:00
										 |  |  | 
 | 
					
						
							|  |  |  |     // check to see if username is on blacklist
 | 
					
						
							|  |  |  |     if (username && blacklistedUsernames.indexOf(username) !== -1) { | 
					
						
							| 
									
										
										
										
											2015-12-28 13:52:28 -08:00
										 |  |  |       return Promise.resolve(true); | 
					
						
							| 
									
										
										
										
											2015-06-11 16:46:31 -07:00
										 |  |  |     } | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2015-06-11 19:11:07 -04:00
										 |  |  |     var where = {}; | 
					
						
							|  |  |  |     if (username) { | 
					
						
							|  |  |  |       where.username = username.toLowerCase(); | 
					
						
							|  |  |  |     } else { | 
					
						
							|  |  |  |       where.email = email ? email.toLowerCase() : email; | 
					
						
							|  |  |  |     } | 
					
						
							| 
									
										
										
										
											2018-06-12 16:50:35 +01:00
										 |  |  |     log('where', where); | 
					
						
							| 
									
										
										
										
											2015-12-28 13:52:28 -08:00
										 |  |  |     return User.count(where) | 
					
						
							|  |  |  |     .then(count => count > 0); | 
					
						
							| 
									
										
										
										
											2015-06-11 19:11:07 -04:00
										 |  |  |   }; | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |   User.remoteMethod( | 
					
						
							|  |  |  |     'doesExist', | 
					
						
							|  |  |  |     { | 
					
						
							|  |  |  |       description: 'checks whether a user exists using email or username', | 
					
						
							|  |  |  |       accepts: [ | 
					
						
							|  |  |  |         { | 
					
						
							|  |  |  |           arg: 'username', | 
					
						
							|  |  |  |           type: 'string' | 
					
						
							|  |  |  |         }, | 
					
						
							|  |  |  |         { | 
					
						
							|  |  |  |           arg: 'email', | 
					
						
							|  |  |  |           type: 'string' | 
					
						
							|  |  |  |         } | 
					
						
							|  |  |  |       ], | 
					
						
							|  |  |  |       returns: [ | 
					
						
							|  |  |  |         { | 
					
						
							|  |  |  |           arg: 'exists', | 
					
						
							|  |  |  |           type: 'boolean' | 
					
						
							|  |  |  |         } | 
					
						
							|  |  |  |       ], | 
					
						
							|  |  |  |       http: { | 
					
						
							|  |  |  |         path: '/exists', | 
					
						
							|  |  |  |         verb: 'get' | 
					
						
							|  |  |  |       } | 
					
						
							|  |  |  |     } | 
					
						
							|  |  |  |   ); | 
					
						
							| 
									
										
										
										
											2015-07-29 11:32:16 -07:00
										 |  |  | 
 | 
					
						
							|  |  |  |   User.about = function about(username, cb) { | 
					
						
							|  |  |  |     if (!username) { | 
					
						
							|  |  |  |       // Zalgo!!
 | 
					
						
							| 
									
										
										
										
											2015-07-29 15:00:24 -07:00
										 |  |  |       return nextTick(() => { | 
					
						
							| 
									
										
										
										
											2018-06-25 20:20:40 +01:00
										 |  |  |         cb(null, {}); | 
					
						
							| 
									
										
										
										
											2015-07-29 11:32:16 -07:00
										 |  |  |       }); | 
					
						
							|  |  |  |     } | 
					
						
							| 
									
										
										
										
											2016-04-14 17:07:40 -07:00
										 |  |  |     return User.findOne({ where: { username } }, (err, user) => { | 
					
						
							|  |  |  |       if (err) { | 
					
						
							|  |  |  |         return cb(err); | 
					
						
							|  |  |  |       } | 
					
						
							|  |  |  |       if (!user || user.username !== username) { | 
					
						
							| 
									
										
										
										
											2018-06-25 20:20:40 +01:00
										 |  |  |         return cb(null, {}); | 
					
						
							| 
									
										
										
										
											2016-04-14 17:07:40 -07:00
										 |  |  |       } | 
					
						
							|  |  |  |       const aboutUser = getAboutProfile(user); | 
					
						
							|  |  |  |       return cb(null, aboutUser); | 
					
						
							|  |  |  |     }); | 
					
						
							| 
									
										
										
										
											2015-07-29 11:32:16 -07:00
										 |  |  |   }; | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |   User.remoteMethod( | 
					
						
							|  |  |  |     'about', | 
					
						
							|  |  |  |     { | 
					
						
							|  |  |  |       description: 'get public info about user', | 
					
						
							|  |  |  |       accepts: [ | 
					
						
							|  |  |  |         { | 
					
						
							|  |  |  |           arg: 'username', | 
					
						
							|  |  |  |           type: 'string' | 
					
						
							|  |  |  |         } | 
					
						
							|  |  |  |       ], | 
					
						
							|  |  |  |       returns: [ | 
					
						
							|  |  |  |         { | 
					
						
							|  |  |  |           arg: 'about', | 
					
						
							|  |  |  |           type: 'object' | 
					
						
							|  |  |  |         } | 
					
						
							|  |  |  |       ], | 
					
						
							|  |  |  |       http: { | 
					
						
							|  |  |  |         path: '/about', | 
					
						
							|  |  |  |         verb: 'get' | 
					
						
							|  |  |  |       } | 
					
						
							|  |  |  |     } | 
					
						
							|  |  |  |   ); | 
					
						
							| 
									
										
										
										
											2015-07-29 15:00:24 -07:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2017-12-29 09:59:27 -08:00
										 |  |  |   User.prototype.createAuthToken = function createAuthToken({ ttl } = {}) { | 
					
						
							|  |  |  |     return Observable.fromNodeCallback( | 
					
						
							|  |  |  |       this.authTokens.create.bind(this.authTokens) | 
					
						
							|  |  |  |     )({ ttl }); | 
					
						
							|  |  |  |   }; | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2018-06-07 22:35:06 +01:00
										 |  |  |   User.prototype.createDonation = function createDonation(donation = {}) { | 
					
						
							|  |  |  |     return Observable.fromNodeCallback( | 
					
						
							|  |  |  |       this.donations.create.bind(this.donations) | 
					
						
							|  |  |  |     )(donation) | 
					
						
							|  |  |  |     .do(() => this.update$({ | 
					
						
							|  |  |  |       $set: { | 
					
						
							|  |  |  |         isDonating: true | 
					
						
							|  |  |  |       }, | 
					
						
							|  |  |  |       $push: { | 
					
						
							|  |  |  |         donationEmails: donation.email | 
					
						
							|  |  |  |         } | 
					
						
							|  |  |  |       }) | 
					
						
							|  |  |  |     ) | 
					
						
							| 
									
										
										
										
											2018-06-12 16:50:35 +01:00
										 |  |  |     .do(() => this.manualReload()); | 
					
						
							| 
									
										
										
										
											2018-06-07 22:35:06 +01:00
										 |  |  |   }; | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2018-05-31 01:36:43 +05:30
										 |  |  |   User.prototype.getEncodedEmail = function getEncodedEmail(email) { | 
					
						
							|  |  |  |     if (!email) { | 
					
						
							| 
									
										
										
										
											2017-12-26 20:12:15 -08:00
										 |  |  |       return null; | 
					
						
							| 
									
										
										
										
											2016-06-26 21:34:01 +05:30
										 |  |  |     } | 
					
						
							| 
									
										
										
										
											2018-05-31 01:36:43 +05:30
										 |  |  |     return Buffer(email).toString('base64'); | 
					
						
							| 
									
										
										
										
											2017-12-26 20:12:15 -08:00
										 |  |  |   }; | 
					
						
							| 
									
										
										
										
											2016-06-26 21:34:01 +05:30
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2017-12-26 20:12:15 -08:00
										 |  |  |   User.decodeEmail = email => Buffer(email, 'base64').toString(); | 
					
						
							| 
									
										
										
										
											2016-10-26 13:22:33 +00:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2018-02-16 23:18:53 +00:00
										 |  |  |   function requestAuthEmail(isSignUp, newEmail) { | 
					
						
							| 
									
										
										
										
											2017-12-26 20:12:15 -08:00
										 |  |  |     return Observable.defer(() => { | 
					
						
							| 
									
										
										
										
											2017-12-27 11:16:53 -08:00
										 |  |  |       const messageOrNull = getWaitMessage(this.emailAuthLinkTTL); | 
					
						
							|  |  |  |       if (messageOrNull) { | 
					
						
							|  |  |  |         throw wrapHandledError( | 
					
						
							|  |  |  |           new Error('request is throttled'), | 
					
						
							|  |  |  |           { | 
					
						
							|  |  |  |             type: 'info', | 
					
						
							|  |  |  |             message: messageOrNull | 
					
						
							|  |  |  |           } | 
					
						
							|  |  |  |         ); | 
					
						
							| 
									
										
										
										
											2017-12-26 20:12:15 -08:00
										 |  |  |       } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |       // create a temporary access token with ttl for 15 minutes
 | 
					
						
							| 
									
										
										
										
											2017-12-29 09:59:27 -08:00
										 |  |  |       return this.createAuthToken({ ttl: 15 * 60 * 1000 }); | 
					
						
							| 
									
										
										
										
											2017-12-26 20:12:15 -08:00
										 |  |  |     }) | 
					
						
							| 
									
										
										
										
											2017-12-27 10:11:17 -08:00
										 |  |  |       .flatMap(token => { | 
					
						
							| 
									
										
										
										
											2017-12-28 20:38:16 -08:00
										 |  |  |         let renderAuthEmail = renderSignInEmail; | 
					
						
							| 
									
										
										
										
											2018-03-09 13:39:44 -06:00
										 |  |  |         let subject = 'Your sign in link for freeCodeCamp.org'; | 
					
						
							| 
									
										
										
										
											2017-12-28 20:38:16 -08:00
										 |  |  |         if (isSignUp) { | 
					
						
							|  |  |  |           renderAuthEmail = renderSignUpEmail; | 
					
						
							| 
									
										
										
										
											2018-03-09 13:39:44 -06:00
										 |  |  |           subject = 'Your sign in link for your new freeCodeCamp.org account'; | 
					
						
							| 
									
										
										
										
											2017-12-28 20:38:16 -08:00
										 |  |  |         } | 
					
						
							| 
									
										
										
										
											2018-02-16 23:18:53 +00:00
										 |  |  |         if (newEmail) { | 
					
						
							|  |  |  |           renderAuthEmail = renderEmailChangeEmail; | 
					
						
							| 
									
										
										
										
											2018-03-09 13:39:44 -06:00
										 |  |  |           subject = dedent`
 | 
					
						
							|  |  |  |             Please confirm your updated email address for freeCodeCamp.org | 
					
						
							|  |  |  |           `;
 | 
					
						
							| 
									
										
										
										
											2018-02-16 23:18:53 +00:00
										 |  |  |         } | 
					
						
							| 
									
										
										
										
											2017-12-27 10:11:17 -08:00
										 |  |  |         const { id: loginToken, created: emailAuthLinkTTL } = token; | 
					
						
							| 
									
										
										
										
											2018-02-16 23:18:53 +00:00
										 |  |  |         const loginEmail = this.getEncodedEmail(newEmail ? newEmail : null); | 
					
						
							| 
									
										
										
										
											2018-08-30 11:27:06 +01:00
										 |  |  |         const host = apiLocation; | 
					
						
							| 
									
										
										
										
											2017-12-27 10:11:17 -08:00
										 |  |  |         const mailOptions = { | 
					
						
							|  |  |  |           type: 'email', | 
					
						
							| 
									
										
										
										
											2018-02-16 23:18:53 +00:00
										 |  |  |           to: newEmail ? newEmail : this.email, | 
					
						
							| 
									
										
										
										
											2017-12-27 10:11:17 -08:00
										 |  |  |           from: getEmailSender(), | 
					
						
							| 
									
										
										
										
											2017-12-28 20:38:16 -08:00
										 |  |  |           subject, | 
					
						
							| 
									
										
										
										
											2017-12-27 10:11:17 -08:00
										 |  |  |           text: renderAuthEmail({ | 
					
						
							|  |  |  |             host, | 
					
						
							|  |  |  |             loginEmail, | 
					
						
							| 
									
										
										
										
											2018-02-16 23:18:53 +00:00
										 |  |  |             loginToken, | 
					
						
							|  |  |  |             emailChange: !!newEmail | 
					
						
							| 
									
										
										
										
											2017-12-27 10:11:17 -08:00
										 |  |  |           }) | 
					
						
							|  |  |  |         }; | 
					
						
							| 
									
										
										
										
											2018-02-16 23:18:53 +00:00
										 |  |  |         return Observable.forkJoin( | 
					
						
							| 
									
										
										
										
											2017-12-27 12:16:17 -08:00
										 |  |  |           User.email.send$(mailOptions), | 
					
						
							| 
									
										
										
										
											2017-12-27 10:11:17 -08:00
										 |  |  |           this.update$({ emailAuthLinkTTL }) | 
					
						
							|  |  |  |         ); | 
					
						
							|  |  |  |       }) | 
					
						
							| 
									
										
										
										
											2018-09-20 10:22:45 +01:00
										 |  |  |       .map(() => 'Check your email and click the link we sent you to confirm' + | 
					
						
							|  |  |  |         ' your new email address.' | 
					
						
							| 
									
										
										
										
											2017-12-28 20:38:16 -08:00
										 |  |  |       ); | 
					
						
							| 
									
										
										
										
											2018-02-16 23:18:53 +00:00
										 |  |  |   } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |   User.prototype.requestAuthEmail = requestAuthEmail; | 
					
						
							| 
									
										
										
										
											2016-06-26 21:34:01 +05:30
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2017-12-27 10:52:13 -08:00
										 |  |  |   User.prototype.requestUpdateEmail = function requestUpdateEmail(newEmail) { | 
					
						
							| 
									
										
										
										
											2018-05-28 18:47:36 +05:30
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2018-02-16 23:18:53 +00:00
										 |  |  |     const currentEmail = this.email; | 
					
						
							| 
									
										
										
										
											2018-05-26 12:54:54 +01:00
										 |  |  |     const isOwnEmail = isTheSame(newEmail, currentEmail); | 
					
						
							| 
									
										
										
										
											2018-05-28 18:47:36 +05:30
										 |  |  |     const isResendUpdateToSameEmail = isTheSame(newEmail, this.newEmail); | 
					
						
							|  |  |  |     const isLinkSentWithinLimit = getWaitMessage(this.emailVerifyTTL); | 
					
						
							|  |  |  |     const isVerifiedEmail = this.emailVerified; | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     if (isOwnEmail && isVerifiedEmail) { | 
					
						
							|  |  |  |       // email is already associated and verified with this account
 | 
					
						
							|  |  |  |       throw wrapHandledError( | 
					
						
							|  |  |  |         new Error('email is already verified'), | 
					
						
							|  |  |  |         { | 
					
						
							|  |  |  |           type: 'info', | 
					
						
							|  |  |  |           message: `
 | 
					
						
							|  |  |  |             ${newEmail} is already associated with this account. | 
					
						
							|  |  |  |             You can update a new email address instead.`
 | 
					
						
							| 
									
										
										
										
											2018-02-16 23:18:53 +00:00
										 |  |  |         } | 
					
						
							| 
									
										
										
										
											2018-05-28 18:47:36 +05:30
										 |  |  |       ); | 
					
						
							| 
									
										
										
										
											2018-05-26 12:54:54 +01:00
										 |  |  |     } | 
					
						
							| 
									
										
										
										
											2018-05-28 18:47:36 +05:30
										 |  |  |     if (isResendUpdateToSameEmail && isLinkSentWithinLimit) { | 
					
						
							| 
									
										
										
										
											2018-05-26 12:54:54 +01:00
										 |  |  |       // trying to update with the same newEmail and
 | 
					
						
							|  |  |  |       // confirmation email is still valid
 | 
					
						
							|  |  |  |       throw wrapHandledError( | 
					
						
							|  |  |  |         new Error(), | 
					
						
							|  |  |  |         { | 
					
						
							|  |  |  |           type: 'info', | 
					
						
							|  |  |  |           message: dedent`
 | 
					
						
							|  |  |  |           We have already sent an email confirmation request to ${newEmail}. | 
					
						
							| 
									
										
										
										
											2018-05-28 18:47:36 +05:30
										 |  |  |           ${isLinkSentWithinLimit}`
 | 
					
						
							| 
									
										
										
										
											2018-05-26 12:54:54 +01:00
										 |  |  |         } | 
					
						
							|  |  |  |       ); | 
					
						
							|  |  |  |     } | 
					
						
							|  |  |  |     if (!isEmail('' + newEmail)) { | 
					
						
							|  |  |  |       throw createEmailError(); | 
					
						
							|  |  |  |     } | 
					
						
							| 
									
										
										
										
											2018-05-28 18:47:36 +05:30
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2018-05-26 12:54:54 +01:00
										 |  |  |     // newEmail is not associated with this user, and
 | 
					
						
							|  |  |  |     // this attempt to change email is the first or
 | 
					
						
							|  |  |  |     // previous attempts have expired
 | 
					
						
							| 
									
										
										
										
											2018-05-28 18:47:36 +05:30
										 |  |  |     if ( | 
					
						
							|  |  |  |         !isOwnEmail || | 
					
						
							|  |  |  |         (isOwnEmail && !isVerifiedEmail) || | 
					
						
							|  |  |  |         (isResendUpdateToSameEmail && !isLinkSentWithinLimit) | 
					
						
							|  |  |  |       ) { | 
					
						
							|  |  |  |       const updateConfig = { | 
					
						
							| 
									
										
										
										
											2018-05-26 12:54:54 +01:00
										 |  |  |         newEmail, | 
					
						
							|  |  |  |         emailVerified: false, | 
					
						
							|  |  |  |         emailVerifyTTL: new Date() | 
					
						
							|  |  |  |       }; | 
					
						
							| 
									
										
										
										
											2018-05-28 19:23:17 +05:30
										 |  |  | 
 | 
					
						
							|  |  |  |       // defer prevents the promise from firing prematurely (before subscribe)
 | 
					
						
							|  |  |  |       return Observable.defer(() => User.doesExist(null, newEmail)) | 
					
						
							|  |  |  |       .do(exists => { | 
					
						
							|  |  |  |         if (exists && !isOwnEmail) { | 
					
						
							|  |  |  |           // newEmail is not associated with this account,
 | 
					
						
							|  |  |  |           // but is associated with different account
 | 
					
						
							|  |  |  |           throw wrapHandledError( | 
					
						
							|  |  |  |             new Error('email already in use'), | 
					
						
							|  |  |  |             { | 
					
						
							|  |  |  |               type: 'info', | 
					
						
							|  |  |  |               message: | 
					
						
							|  |  |  |               `${newEmail} is already associated with another account.` | 
					
						
							|  |  |  |             } | 
					
						
							|  |  |  |           ); | 
					
						
							|  |  |  |         } | 
					
						
							|  |  |  |       }) | 
					
						
							|  |  |  |       .flatMap(()=>{ | 
					
						
							| 
									
										
										
										
											2018-09-20 10:22:45 +01:00
										 |  |  |         const updatePromise = new Promise((resolve, reject) => | 
					
						
							|  |  |  |         this.updateAttributes(updateConfig, err => { | 
					
						
							|  |  |  |           if (err) { | 
					
						
							|  |  |  |             return reject(err); | 
					
						
							|  |  |  |           } | 
					
						
							|  |  |  |           return resolve(); | 
					
						
							|  |  |  |         })); | 
					
						
							| 
									
										
										
										
											2018-05-28 19:23:17 +05:30
										 |  |  |         return Observable.forkJoin( | 
					
						
							| 
									
										
										
										
											2018-09-20 10:22:45 +01:00
										 |  |  |           Observable.fromPromise(updatePromise), | 
					
						
							| 
									
										
										
										
											2018-05-28 19:23:17 +05:30
										 |  |  |           this.requestAuthEmail(false, newEmail), | 
					
						
							| 
									
										
										
										
											2018-05-29 01:59:37 +05:30
										 |  |  |           (_, message) => message | 
					
						
							| 
									
										
										
										
											2018-09-20 10:22:45 +01:00
										 |  |  |         ); | 
					
						
							| 
									
										
										
										
											2018-05-28 19:23:17 +05:30
										 |  |  |       }); | 
					
						
							| 
									
										
										
										
											2018-05-28 18:47:36 +05:30
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2018-05-26 12:54:54 +01:00
										 |  |  |     } else { | 
					
						
							|  |  |  |       return 'Something unexpected happened whilst updating your email.'; | 
					
						
							|  |  |  |     } | 
					
						
							| 
									
										
										
										
											2018-02-16 23:18:53 +00:00
										 |  |  |   }; | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2018-05-15 14:56:26 +01:00
										 |  |  |   function requestCompletedChallenges() { | 
					
						
							|  |  |  |     return this.getCompletedChallenges$(); | 
					
						
							|  |  |  |   } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |   User.prototype.requestCompletedChallenges = requestCompletedChallenges; | 
					
						
							| 
									
										
										
										
											2018-02-16 23:18:53 +00:00
										 |  |  | 
 | 
					
						
							|  |  |  |   User.prototype.requestUpdateFlags = function requestUpdateFlags(values) { | 
					
						
							|  |  |  |     const flagsToCheck = Object.keys(values); | 
					
						
							|  |  |  |     const valuesToCheck = _.pick({ ...this }, flagsToCheck); | 
					
						
							|  |  |  |     const valuesToUpdate = flagsToCheck | 
					
						
							|  |  |  |       .filter(flag => !isTheSame(values[flag], valuesToCheck[flag])); | 
					
						
							|  |  |  |     if (!valuesToUpdate.length) { | 
					
						
							|  |  |  |       return Observable.of(dedent`
 | 
					
						
							|  |  |  |         No property in | 
					
						
							|  |  |  |         ${JSON.stringify(flagsToCheck, null, 2)} | 
					
						
							|  |  |  |         will introduce a change in this user. | 
					
						
							|  |  |  |         `
 | 
					
						
							|  |  |  |       ) | 
					
						
							| 
									
										
										
										
											2018-05-24 12:19:51 +01:00
										 |  |  |        .map(() => dedent`Your settings have not been updated.`); | 
					
						
							| 
									
										
										
										
											2018-02-16 23:18:53 +00:00
										 |  |  |     } | 
					
						
							|  |  |  |     return Observable.from(valuesToUpdate) | 
					
						
							|  |  |  |       .flatMap(flag => Observable.of({ flag, newValue: values[flag] })) | 
					
						
							|  |  |  |       .toArray() | 
					
						
							|  |  |  |       .flatMap(updates => { | 
					
						
							|  |  |  |         return Observable.forkJoin( | 
					
						
							|  |  |  |           Observable.from(updates) | 
					
						
							|  |  |  |             .flatMap(({ flag, newValue }) => { | 
					
						
							|  |  |  |               return Observable.fromPromise(User.doesExist(null, this.email)) | 
					
						
							| 
									
										
										
										
											2018-06-12 16:50:35 +01:00
										 |  |  |                 .flatMap(() => this.update$({ [flag]: newValue })); | 
					
						
							| 
									
										
										
										
											2018-02-16 23:18:53 +00:00
										 |  |  |             }) | 
					
						
							|  |  |  |         ); | 
					
						
							| 
									
										
										
										
											2016-07-19 16:36:34 -07:00
										 |  |  |       }) | 
					
						
							| 
									
										
										
										
											2018-06-12 16:50:35 +01:00
										 |  |  |       .doOnNext(() => this.manualReload()) | 
					
						
							| 
									
										
										
										
											2016-07-19 16:36:34 -07:00
										 |  |  |       .map(() => dedent`
 | 
					
						
							| 
									
										
										
										
											2018-02-16 23:18:53 +00:00
										 |  |  |         We have successfully updated your account. | 
					
						
							| 
									
										
										
										
											2016-07-19 16:36:34 -07:00
										 |  |  |       `);
 | 
					
						
							| 
									
										
										
										
											2016-04-22 02:17:59 +05:30
										 |  |  |   }; | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2018-02-16 23:18:53 +00:00
										 |  |  |   User.prototype.updateMyPortfolio = | 
					
						
							|  |  |  |     function updateMyPortfolio(portfolioItem, deleteRequest) { | 
					
						
							|  |  |  |       const currentPortfolio = this.portfolio.slice(0); | 
					
						
							|  |  |  |       const pIndex = _.findIndex( | 
					
						
							|  |  |  |         currentPortfolio, | 
					
						
							|  |  |  |         p => p.id === portfolioItem.id | 
					
						
							|  |  |  |       ); | 
					
						
							|  |  |  |       let updatedPortfolio = []; | 
					
						
							|  |  |  |       if (deleteRequest) { | 
					
						
							|  |  |  |         updatedPortfolio = currentPortfolio.filter( | 
					
						
							|  |  |  |           p => p.id !== portfolioItem.id | 
					
						
							|  |  |  |         ); | 
					
						
							|  |  |  |       } else if (pIndex === -1) { | 
					
						
							|  |  |  |         updatedPortfolio = currentPortfolio.concat([ portfolioItem ]); | 
					
						
							|  |  |  |       } else { | 
					
						
							|  |  |  |         updatedPortfolio = [ ...currentPortfolio ]; | 
					
						
							|  |  |  |         updatedPortfolio[pIndex] = { ...portfolioItem }; | 
					
						
							|  |  |  |       } | 
					
						
							|  |  |  |       return this.update$({ portfolio: updatedPortfolio }) | 
					
						
							| 
									
										
										
										
											2018-06-12 16:50:35 +01:00
										 |  |  |         .do(() => this.manualReload()) | 
					
						
							| 
									
										
										
										
											2018-02-16 23:18:53 +00:00
										 |  |  |         .map(() => dedent`
 | 
					
						
							| 
									
										
										
										
											2018-03-09 13:39:44 -06:00
										 |  |  |           Your portfolio has been updated. | 
					
						
							| 
									
										
										
										
											2018-02-16 23:18:53 +00:00
										 |  |  |         `);
 | 
					
						
							|  |  |  |     }; | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |   User.prototype.updateMyProjects = function updateMyProjects(project) { | 
					
						
							| 
									
										
										
										
											2018-05-24 14:59:46 +01:00
										 |  |  |     const updateData = { $set: {} }; | 
					
						
							| 
									
										
										
										
											2018-05-15 14:56:26 +01:00
										 |  |  |     return this.getCompletedChallenges$() | 
					
						
							| 
									
										
										
										
											2018-05-24 14:59:46 +01:00
										 |  |  |       .flatMap(() => { | 
					
						
							|  |  |  |         const { | 
					
						
							|  |  |  |           updated, | 
					
						
							|  |  |  |           isNewCompletionCount | 
					
						
							|  |  |  |         } = buildCompletedChallengesUpdate( | 
					
						
							|  |  |  |           this.completedChallenges, | 
					
						
							| 
									
										
										
										
											2018-02-16 23:18:53 +00:00
										 |  |  |           project | 
					
						
							|  |  |  |         ); | 
					
						
							| 
									
										
										
										
											2018-05-24 14:59:46 +01:00
										 |  |  |         updateData.$set.completedChallenges = updated; | 
					
						
							|  |  |  |         if (isNewCompletionCount) { | 
					
						
							|  |  |  |           let points = []; | 
					
						
							|  |  |  |           // give points a length of isNewCompletionCount
 | 
					
						
							|  |  |  |           points[isNewCompletionCount - 1] = true; | 
					
						
							|  |  |  |           updateData.$push = {}; | 
					
						
							|  |  |  |           updateData.$push.progressTimestamps = { | 
					
						
							|  |  |  |             $each: points.map(() => Date.now()) | 
					
						
							|  |  |  |           }; | 
					
						
							|  |  |  |         } | 
					
						
							| 
									
										
										
										
											2018-02-16 23:18:53 +00:00
										 |  |  |         return this.update$(updateData); | 
					
						
							|  |  |  |       }) | 
					
						
							| 
									
										
										
										
											2018-06-12 16:50:35 +01:00
										 |  |  |       .doOnNext(() => this.manualReload() ) | 
					
						
							| 
									
										
										
										
											2018-02-16 23:18:53 +00:00
										 |  |  |       .map(() => dedent`
 | 
					
						
							| 
									
										
										
										
											2018-03-09 13:39:44 -06:00
										 |  |  |         Your projects have been updated. | 
					
						
							| 
									
										
										
										
											2018-02-16 23:18:53 +00:00
										 |  |  |       `);
 | 
					
						
							|  |  |  |   }; | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2018-05-20 04:07:41 +01:00
										 |  |  |   User.prototype.updateMyProfileUI = function updateMyProfileUI(profileUI) { | 
					
						
							|  |  |  |     const oldUI = { ...this.profileUI }; | 
					
						
							|  |  |  |     const update = { | 
					
						
							|  |  |  |       profileUI: { | 
					
						
							|  |  |  |         ...oldUI, | 
					
						
							|  |  |  |         ...profileUI | 
					
						
							|  |  |  |       } | 
					
						
							|  |  |  |     }; | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     return this.update$(update) | 
					
						
							| 
									
										
										
										
											2018-06-12 16:50:35 +01:00
										 |  |  |       .doOnNext(() => this.manualReload()) | 
					
						
							| 
									
										
										
										
											2018-05-20 04:07:41 +01:00
										 |  |  |       .map(() => dedent`
 | 
					
						
							|  |  |  |         Your privacy settings have been updated. | 
					
						
							|  |  |  |       `);
 | 
					
						
							|  |  |  |   }; | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2018-02-16 23:18:53 +00:00
										 |  |  |   User.prototype.updateMyUsername = function updateMyUsername(newUsername) { | 
					
						
							|  |  |  |     return Observable.defer( | 
					
						
							|  |  |  |       () => { | 
					
						
							|  |  |  |         const isOwnUsername = isTheSame(newUsername, this.username); | 
					
						
							|  |  |  |         if (isOwnUsername) { | 
					
						
							|  |  |  |           return Observable.of(dedent`
 | 
					
						
							| 
									
										
										
										
											2018-03-09 13:39:44 -06:00
										 |  |  |           ${newUsername} is already associated with this account. | 
					
						
							| 
									
										
										
										
											2018-02-16 23:18:53 +00:00
										 |  |  |           `);
 | 
					
						
							|  |  |  |         } | 
					
						
							|  |  |  |         return Observable.fromPromise(User.doesExist(newUsername)); | 
					
						
							|  |  |  |       } | 
					
						
							|  |  |  |     ) | 
					
						
							|  |  |  |     .flatMap(boolOrMessage => { | 
					
						
							|  |  |  |       if (typeof boolOrMessage === 'string') { | 
					
						
							|  |  |  |         return Observable.of(boolOrMessage); | 
					
						
							|  |  |  |       } | 
					
						
							|  |  |  |       if (boolOrMessage) { | 
					
						
							|  |  |  |         return Observable.of(dedent`
 | 
					
						
							| 
									
										
										
										
											2018-03-09 13:39:44 -06:00
										 |  |  |         ${newUsername} is already associated with a different account. | 
					
						
							| 
									
										
										
										
											2018-02-16 23:18:53 +00:00
										 |  |  |         `);
 | 
					
						
							|  |  |  |       } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |       return this.update$({ username: newUsername }) | 
					
						
							| 
									
										
										
										
											2018-06-12 16:50:35 +01:00
										 |  |  |         .do(() => this.manualReload()) | 
					
						
							| 
									
										
										
										
											2018-02-16 23:18:53 +00:00
										 |  |  |         .map(() => dedent`
 | 
					
						
							| 
									
										
										
										
											2018-03-09 13:39:44 -06:00
										 |  |  |         Your username has been updated successfully. | 
					
						
							| 
									
										
										
										
											2018-02-16 23:18:53 +00:00
										 |  |  |         `);
 | 
					
						
							|  |  |  |     }); | 
					
						
							|  |  |  |   }; | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2018-06-20 15:19:39 +01:00
										 |  |  |   function prepUserForPublish(user, profileUI) { | 
					
						
							|  |  |  |     const { | 
					
						
							|  |  |  |       about, | 
					
						
							|  |  |  |       calendar, | 
					
						
							|  |  |  |       completedChallenges, | 
					
						
							|  |  |  |       isDonating, | 
					
						
							|  |  |  |       location, | 
					
						
							|  |  |  |       name, | 
					
						
							|  |  |  |       points, | 
					
						
							|  |  |  |       portfolio, | 
					
						
							|  |  |  |       streak, | 
					
						
							| 
									
										
										
										
											2018-08-02 18:39:51 +03:00
										 |  |  |       username, | 
					
						
							|  |  |  |       yearsTopContributor | 
					
						
							| 
									
										
										
										
											2018-06-20 15:19:39 +01:00
										 |  |  |     } = user; | 
					
						
							|  |  |  |     const { | 
					
						
							|  |  |  |       isLocked = true, | 
					
						
							|  |  |  |       showAbout = false, | 
					
						
							|  |  |  |       showCerts = false, | 
					
						
							|  |  |  |       showDonation = false, | 
					
						
							|  |  |  |       showHeatMap = false, | 
					
						
							|  |  |  |       showLocation = false, | 
					
						
							|  |  |  |       showName = false, | 
					
						
							|  |  |  |       showPoints = false, | 
					
						
							|  |  |  |       showPortfolio = false, | 
					
						
							|  |  |  |       showTimeLine = false | 
					
						
							|  |  |  |     } = profileUI; | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     if (isLocked) { | 
					
						
							|  |  |  |       return { | 
					
						
							|  |  |  |         isLocked, | 
					
						
							| 
									
										
										
										
											2018-08-15 15:30:27 +05:30
										 |  |  |         profileUI, | 
					
						
							| 
									
										
										
										
											2018-06-20 15:19:39 +01:00
										 |  |  |         username | 
					
						
							|  |  |  |       }; | 
					
						
							|  |  |  |     } | 
					
						
							|  |  |  |     return { | 
					
						
							|  |  |  |       ...user, | 
					
						
							|  |  |  |       about: showAbout ? about : '', | 
					
						
							|  |  |  |       calendar: showHeatMap ? calendar : {}, | 
					
						
							|  |  |  |       completedChallenges: showCerts && showTimeLine ? completedChallenges : [], | 
					
						
							|  |  |  |       isDonating: showDonation ? isDonating : null, | 
					
						
							|  |  |  |       location: showLocation ? location : '', | 
					
						
							|  |  |  |       name: showName ? name : '', | 
					
						
							|  |  |  |       points: showPoints ? points : null, | 
					
						
							|  |  |  |       portfolio: showPortfolio ? portfolio : [], | 
					
						
							| 
									
										
										
										
											2018-08-02 18:39:51 +03:00
										 |  |  |       streak: showHeatMap ? streak : {}, | 
					
						
							|  |  |  |       yearsTopContributor: yearsTopContributor | 
					
						
							| 
									
										
										
										
											2018-06-20 15:19:39 +01:00
										 |  |  |     }; | 
					
						
							|  |  |  |   } | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2018-02-19 20:32:14 +00:00
										 |  |  |   User.getPublicProfile = function getPublicProfile(username, cb) { | 
					
						
							|  |  |  |     return User.findOne$({ where: { username }}) | 
					
						
							|  |  |  |       .flatMap(user => { | 
					
						
							|  |  |  |         if (!user) { | 
					
						
							|  |  |  |           return Observable.of({}); | 
					
						
							|  |  |  |         } | 
					
						
							| 
									
										
										
										
											2018-06-20 15:19:39 +01:00
										 |  |  |         const { | 
					
						
							|  |  |  |           completedChallenges, | 
					
						
							|  |  |  |           progressTimestamps, | 
					
						
							|  |  |  |           timezone, | 
					
						
							|  |  |  |           profileUI | 
					
						
							|  |  |  |         } = user; | 
					
						
							|  |  |  |         const allUser = { | 
					
						
							|  |  |  |           ..._.pick(user, publicUserProps), | 
					
						
							|  |  |  |           isGithub: !!user.githubProfile, | 
					
						
							|  |  |  |           isLinkedIn: !!user.linkedIn, | 
					
						
							|  |  |  |           isTwitter: !!user.twitter, | 
					
						
							|  |  |  |           isWebsite: !!user.website, | 
					
						
							|  |  |  |           points: progressTimestamps.length, | 
					
						
							|  |  |  |           completedChallenges, | 
					
						
							|  |  |  |           ...getProgress(progressTimestamps, timezone), | 
					
						
							|  |  |  |           ...normaliseUserFields(user) | 
					
						
							|  |  |  |         }; | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |         const publicUser = prepUserForPublish(allUser, profileUI); | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2018-02-19 20:32:14 +00:00
										 |  |  |         return Observable.of({ | 
					
						
							|  |  |  |           entities: { | 
					
						
							|  |  |  |             user: { | 
					
						
							|  |  |  |               [user.username]: { | 
					
						
							| 
									
										
										
										
											2018-06-20 15:19:39 +01:00
										 |  |  |                 ...publicUser | 
					
						
							| 
									
										
										
										
											2018-02-19 20:32:14 +00:00
										 |  |  |               } | 
					
						
							|  |  |  |             } | 
					
						
							|  |  |  |           }, | 
					
						
							|  |  |  |           result: user.username | 
					
						
							|  |  |  |         }); | 
					
						
							|  |  |  |       }) | 
					
						
							|  |  |  |       .subscribe( | 
					
						
							|  |  |  |         user => cb(null, user), | 
					
						
							|  |  |  |         cb | 
					
						
							|  |  |  |       ); | 
					
						
							|  |  |  |   }; | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |   User.remoteMethod('getPublicProfile', { | 
					
						
							|  |  |  |     accepts: { | 
					
						
							|  |  |  |       arg: 'username', | 
					
						
							|  |  |  |       type: 'string', | 
					
						
							|  |  |  |       required: true | 
					
						
							|  |  |  |     }, | 
					
						
							|  |  |  |     returns: [ | 
					
						
							|  |  |  |       { | 
					
						
							|  |  |  |         arg: 'user', | 
					
						
							|  |  |  |         type: 'object', | 
					
						
							|  |  |  |         root: true | 
					
						
							|  |  |  |       } | 
					
						
							|  |  |  |     ], | 
					
						
							|  |  |  |     http: { | 
					
						
							|  |  |  |       path: '/get-public-profile', | 
					
						
							|  |  |  |       verb: 'GET' | 
					
						
							|  |  |  |     } | 
					
						
							|  |  |  |   }); | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2015-07-29 15:00:24 -07:00
										 |  |  |   User.giveBrowniePoints = | 
					
						
							| 
									
										
										
										
											2015-08-01 20:08:32 -07:00
										 |  |  |     function giveBrowniePoints(receiver, giver, data = {}, dev = false, cb) { | 
					
						
							| 
									
										
										
										
											2016-04-14 17:07:40 -07:00
										 |  |  |       const findUser = observeMethod(User, 'findOne'); | 
					
						
							| 
									
										
										
										
											2015-07-29 15:00:24 -07:00
										 |  |  |       if (!receiver) { | 
					
						
							|  |  |  |         return nextTick(() => { | 
					
						
							| 
									
										
										
										
											2015-07-31 13:45:21 -07:00
										 |  |  |           cb( | 
					
						
							|  |  |  |             new TypeError(`receiver should be a string but got ${ receiver }`) | 
					
						
							|  |  |  |           ); | 
					
						
							| 
									
										
										
										
											2015-07-29 15:00:24 -07:00
										 |  |  |         }); | 
					
						
							|  |  |  |       } | 
					
						
							|  |  |  |       if (!giver) { | 
					
						
							|  |  |  |         return nextTick(() => { | 
					
						
							| 
									
										
										
										
											2015-07-31 13:45:21 -07:00
										 |  |  |           cb(new TypeError(`giver should be a string but got ${ giver }`)); | 
					
						
							| 
									
										
										
										
											2015-07-29 15:00:24 -07:00
										 |  |  |         }); | 
					
						
							|  |  |  |       } | 
					
						
							| 
									
										
										
										
											2015-07-31 12:45:34 -07:00
										 |  |  |       let temp = moment(); | 
					
						
							|  |  |  |       const browniePoints = temp | 
					
						
							|  |  |  |         .subtract.apply(temp, BROWNIEPOINTS_TIMEOUT) | 
					
						
							|  |  |  |         .valueOf(); | 
					
						
							| 
									
										
										
										
											2016-04-14 17:07:40 -07:00
										 |  |  |       const user$ = findUser({ where: { username: receiver }}); | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |       return user$ | 
					
						
							|  |  |  |         .tapOnNext((user) => { | 
					
						
							| 
									
										
										
										
											2015-07-29 15:00:24 -07:00
										 |  |  |           if (!user) { | 
					
						
							| 
									
										
										
										
											2015-07-31 13:45:21 -07:00
										 |  |  |             throw new Error(`could not find receiver for ${ receiver }`); | 
					
						
							| 
									
										
										
										
											2015-07-29 15:00:24 -07:00
										 |  |  |           } | 
					
						
							|  |  |  |         }) | 
					
						
							|  |  |  |         .flatMap(({ progressTimestamps = [] }) => { | 
					
						
							| 
									
										
										
										
											2016-04-14 17:07:40 -07:00
										 |  |  |           return Observable.from(progressTimestamps); | 
					
						
							| 
									
										
										
										
											2015-07-29 15:00:24 -07:00
										 |  |  |         }) | 
					
						
							|  |  |  |         // filter out non objects
 | 
					
						
							|  |  |  |         .filter((timestamp) => !!timestamp || typeof timestamp === 'object') | 
					
						
							|  |  |  |         // filterout timestamps older then an hour
 | 
					
						
							| 
									
										
										
										
											2016-04-14 17:07:40 -07:00
										 |  |  |         .filter(({ timestamp = 0 }) => { | 
					
						
							|  |  |  |           return timestamp >= browniePoints; | 
					
						
							|  |  |  |         }) | 
					
						
							| 
									
										
										
										
											2015-07-29 15:00:24 -07:00
										 |  |  |         // filter out brownie points given by giver
 | 
					
						
							| 
									
										
										
										
											2016-04-14 17:07:40 -07:00
										 |  |  |         .filter((browniePoint) => { | 
					
						
							|  |  |  |           return browniePoint.giver === giver; | 
					
						
							|  |  |  |         }) | 
					
						
							| 
									
										
										
										
											2015-07-29 15:00:24 -07:00
										 |  |  |         // no results means this is the first brownie point given by giver
 | 
					
						
							|  |  |  |         // so return -1 to indicate receiver should receive point
 | 
					
						
							| 
									
										
										
										
											2015-10-15 00:33:45 -07:00
										 |  |  |         .first({ defaultValue: -1 }) | 
					
						
							| 
									
										
										
										
											2016-04-14 17:07:40 -07:00
										 |  |  |         .flatMap((browniePointsFromGiver) => { | 
					
						
							| 
									
										
										
										
											2015-07-29 15:00:24 -07:00
										 |  |  |           if (browniePointsFromGiver === -1) { | 
					
						
							| 
									
										
										
										
											2016-04-14 17:07:40 -07:00
										 |  |  | 
 | 
					
						
							|  |  |  |             return user$.flatMap((user) => { | 
					
						
							|  |  |  |               user.progressTimestamps.push({ | 
					
						
							|  |  |  |                 giver, | 
					
						
							|  |  |  |                 timestamp: Date.now(), | 
					
						
							|  |  |  |                 ...data | 
					
						
							| 
									
										
										
										
											2015-07-29 15:00:24 -07:00
										 |  |  |               }); | 
					
						
							| 
									
										
										
										
											2016-04-14 17:07:40 -07:00
										 |  |  |               return saveUser(user); | 
					
						
							|  |  |  |             }); | 
					
						
							| 
									
										
										
										
											2015-07-29 15:00:24 -07:00
										 |  |  |           } | 
					
						
							|  |  |  |           return Observable.throw( | 
					
						
							| 
									
										
										
										
											2015-07-31 13:45:21 -07:00
										 |  |  |             new Error(`${ giver } already gave ${ receiver } points`) | 
					
						
							| 
									
										
										
										
											2015-07-29 15:00:24 -07:00
										 |  |  |           ); | 
					
						
							|  |  |  |         }) | 
					
						
							|  |  |  |         .subscribe( | 
					
						
							| 
									
										
										
										
											2016-04-14 17:07:40 -07:00
										 |  |  |           (user) => { | 
					
						
							|  |  |  |             return cb( | 
					
						
							|  |  |  |               null, | 
					
						
							|  |  |  |               getAboutProfile(user), | 
					
						
							|  |  |  |               dev ? | 
					
						
							|  |  |  |                 { giver, receiver, data } : | 
					
						
							|  |  |  |                 null | 
					
						
							|  |  |  |             ); | 
					
						
							|  |  |  |           }, | 
					
						
							|  |  |  |           (e) => cb(e, null, dev ? { giver, receiver, data } : null), | 
					
						
							| 
									
										
										
										
											2015-07-29 15:00:24 -07:00
										 |  |  |           () => { | 
					
						
							| 
									
										
										
										
											2018-06-12 16:50:35 +01:00
										 |  |  |             log('brownie points assigned completed'); | 
					
						
							| 
									
										
										
										
											2015-07-29 15:00:24 -07:00
										 |  |  |           } | 
					
						
							|  |  |  |         ); | 
					
						
							|  |  |  |     }; | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |   User.remoteMethod( | 
					
						
							|  |  |  |     'giveBrowniePoints', | 
					
						
							|  |  |  |     { | 
					
						
							|  |  |  |       description: 'Give this user brownie points', | 
					
						
							|  |  |  |       accepts: [ | 
					
						
							|  |  |  |         { | 
					
						
							|  |  |  |           arg: 'receiver', | 
					
						
							|  |  |  |           type: 'string', | 
					
						
							|  |  |  |           required: true | 
					
						
							|  |  |  |         }, | 
					
						
							|  |  |  |         { | 
					
						
							|  |  |  |           arg: 'giver', | 
					
						
							|  |  |  |           type: 'string', | 
					
						
							|  |  |  |           required: true | 
					
						
							|  |  |  |         }, | 
					
						
							|  |  |  |         { | 
					
						
							|  |  |  |           arg: 'data', | 
					
						
							|  |  |  |           type: 'object' | 
					
						
							| 
									
										
										
										
											2015-08-01 20:08:32 -07:00
										 |  |  |         }, | 
					
						
							|  |  |  |         { | 
					
						
							|  |  |  |           arg: 'debug', | 
					
						
							|  |  |  |           type: 'boolean' | 
					
						
							| 
									
										
										
										
											2015-07-29 15:00:24 -07:00
										 |  |  |         } | 
					
						
							|  |  |  |       ], | 
					
						
							|  |  |  |       returns: [ | 
					
						
							|  |  |  |         { | 
					
						
							|  |  |  |           arg: 'about', | 
					
						
							|  |  |  |           type: 'object' | 
					
						
							| 
									
										
										
										
											2015-08-01 20:08:32 -07:00
										 |  |  |         }, | 
					
						
							|  |  |  |         { | 
					
						
							|  |  |  |           arg: 'debug', | 
					
						
							|  |  |  |           type: 'object' | 
					
						
							| 
									
										
										
										
											2015-07-29 15:00:24 -07:00
										 |  |  |         } | 
					
						
							|  |  |  |       ], | 
					
						
							|  |  |  |       http: { | 
					
						
							|  |  |  |         path: '/give-brownie-points', | 
					
						
							| 
									
										
										
										
											2015-08-01 20:08:32 -07:00
										 |  |  |         verb: 'POST' | 
					
						
							| 
									
										
										
										
											2015-07-29 15:00:24 -07:00
										 |  |  |       } | 
					
						
							|  |  |  |     } | 
					
						
							| 
									
										
										
										
											2015-07-31 12:15:23 -07:00
										 |  |  |   ); | 
					
						
							| 
									
										
										
										
											2016-02-09 14:33:25 -08:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2017-12-07 16:13:19 -08:00
										 |  |  |   User.themes = themes; | 
					
						
							| 
									
										
										
										
											2016-05-07 17:46:39 +05:30
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2016-05-12 18:52:03 -07:00
										 |  |  |   User.prototype.updateTheme = function updateTheme(theme) { | 
					
						
							|  |  |  |     if (!this.constructor.themes[theme]) { | 
					
						
							| 
									
										
										
										
											2017-07-13 11:39:07 -07:00
										 |  |  |       const err = wrapHandledError( | 
					
						
							|  |  |  |         new Error('Theme is not valid.'), | 
					
						
							|  |  |  |         { | 
					
						
							|  |  |  |           Type: 'info', | 
					
						
							|  |  |  |           message: err.message | 
					
						
							|  |  |  |         } | 
					
						
							| 
									
										
										
										
											2016-05-12 18:52:03 -07:00
										 |  |  |       ); | 
					
						
							|  |  |  |       return Promise.reject(err); | 
					
						
							|  |  |  |     } | 
					
						
							| 
									
										
										
										
											2018-06-12 16:50:35 +01:00
										 |  |  |     return this.update$({ theme }) | 
					
						
							|  |  |  |       .doOnNext(() => this.manualReload()) | 
					
						
							|  |  |  |       .toPromise(); | 
					
						
							| 
									
										
										
										
											2016-05-12 18:52:03 -07:00
										 |  |  |   }; | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2016-08-05 14:05:57 -07:00
										 |  |  |   // deprecated. remove once live
 | 
					
						
							| 
									
										
										
										
											2016-05-12 18:52:03 -07:00
										 |  |  |   User.remoteMethod( | 
					
						
							|  |  |  |     'updateTheme', | 
					
						
							|  |  |  |     { | 
					
						
							|  |  |  |       description: 'updates the users chosen theme', | 
					
						
							|  |  |  |       accepts: [ | 
					
						
							|  |  |  |         { | 
					
						
							|  |  |  |           arg: 'theme', | 
					
						
							|  |  |  |           type: 'string', | 
					
						
							|  |  |  |           required: true | 
					
						
							|  |  |  |         } | 
					
						
							|  |  |  |       ], | 
					
						
							|  |  |  |       returns: [ | 
					
						
							|  |  |  |         { | 
					
						
							|  |  |  |           arg: 'status', | 
					
						
							|  |  |  |           type: 'object' | 
					
						
							|  |  |  |         } | 
					
						
							|  |  |  |       ], | 
					
						
							|  |  |  |       http: { | 
					
						
							|  |  |  |         path: '/update-theme', | 
					
						
							|  |  |  |         verb: 'POST' | 
					
						
							|  |  |  |       } | 
					
						
							|  |  |  |     } | 
					
						
							|  |  |  |   ); | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2016-04-14 17:07:40 -07:00
										 |  |  |   // user.updateTo$(updateData: Object) => Observable[Number]
 | 
					
						
							| 
									
										
										
										
											2016-02-09 14:33:25 -08:00
										 |  |  |   User.prototype.update$ = function update$(updateData) { | 
					
						
							|  |  |  |     const id = this.getId(); | 
					
						
							|  |  |  |     const updateOptions = { allowExtendedOperators: true }; | 
					
						
							|  |  |  |     if ( | 
					
						
							|  |  |  |         !updateData || | 
					
						
							|  |  |  |         typeof updateData !== 'object' || | 
					
						
							| 
									
										
										
										
											2016-02-10 10:05:51 -08:00
										 |  |  |         !Object.keys(updateData).length | 
					
						
							| 
									
										
										
										
											2016-02-09 14:33:25 -08:00
										 |  |  |     ) { | 
					
						
							|  |  |  |       return Observable.throw(new Error( | 
					
						
							| 
									
										
										
										
											2016-02-10 10:05:51 -08:00
										 |  |  |         dedent`
 | 
					
						
							|  |  |  |           updateData must be an object with at least one key, | 
					
						
							|  |  |  |           but got ${updateData} with ${Object.keys(updateData).length} | 
					
						
							|  |  |  |         `.split('\n').join(' ')
 | 
					
						
							| 
									
										
										
										
											2016-02-09 14:33:25 -08:00
										 |  |  |       )); | 
					
						
							|  |  |  |     } | 
					
						
							|  |  |  |     return this.constructor.update$({ id }, updateData, updateOptions); | 
					
						
							|  |  |  |   }; | 
					
						
							| 
									
										
										
										
											2016-04-14 17:07:40 -07:00
										 |  |  |   User.prototype.getPoints$ = function getPoints$() { | 
					
						
							| 
									
										
										
										
											2016-04-06 21:08:19 -07:00
										 |  |  |     const id = this.getId(); | 
					
						
							|  |  |  |     const filter = { | 
					
						
							|  |  |  |       where: { id }, | 
					
						
							|  |  |  |       fields: { progressTimestamps: true } | 
					
						
							|  |  |  |     }; | 
					
						
							|  |  |  |     return this.constructor.findOne$(filter) | 
					
						
							|  |  |  |       .map(user => { | 
					
						
							|  |  |  |         this.progressTimestamps = user.progressTimestamps; | 
					
						
							|  |  |  |         return user.progressTimestamps; | 
					
						
							|  |  |  |       }); | 
					
						
							|  |  |  |   }; | 
					
						
							| 
									
										
										
										
											2018-05-15 14:56:26 +01:00
										 |  |  |   User.prototype.getCompletedChallenges$ = function getCompletedChallenges$() { | 
					
						
							| 
									
										
										
										
											2016-04-06 21:08:19 -07:00
										 |  |  |     const id = this.getId(); | 
					
						
							|  |  |  |     const filter = { | 
					
						
							|  |  |  |       where: { id }, | 
					
						
							| 
									
										
										
										
											2018-05-15 14:56:26 +01:00
										 |  |  |       fields: { completedChallenges: true } | 
					
						
							| 
									
										
										
										
											2016-04-06 21:08:19 -07:00
										 |  |  |     }; | 
					
						
							|  |  |  |     return this.constructor.findOne$(filter) | 
					
						
							|  |  |  |       .map(user => { | 
					
						
							| 
									
										
										
										
											2018-05-15 14:56:26 +01:00
										 |  |  |         this.completedChallenges = user.completedChallenges; | 
					
						
							|  |  |  |         return user.completedChallenges; | 
					
						
							| 
									
										
										
										
											2016-04-06 21:08:19 -07:00
										 |  |  |       }); | 
					
						
							|  |  |  |   }; | 
					
						
							| 
									
										
										
										
											2018-01-09 17:11:07 -08:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2018-01-12 11:09:09 -08:00
										 |  |  |   User.getMessages = messages => Promise.resolve(messages); | 
					
						
							| 
									
										
										
										
											2018-01-09 17:11:07 -08:00
										 |  |  | 
 | 
					
						
							|  |  |  |   User.remoteMethod('getMessages', { | 
					
						
							|  |  |  |     http: { | 
					
						
							|  |  |  |       verb: 'get', | 
					
						
							|  |  |  |       path: '/get-messages' | 
					
						
							|  |  |  |     }, | 
					
						
							|  |  |  |     accepts: { | 
					
						
							|  |  |  |       arg: 'messages', | 
					
						
							|  |  |  |       type: 'object', | 
					
						
							| 
									
										
										
										
											2018-01-12 11:09:09 -08:00
										 |  |  |       http: ctx => ctx.req.flash() | 
					
						
							| 
									
										
										
										
											2018-01-09 17:11:07 -08:00
										 |  |  |     }, | 
					
						
							|  |  |  |     returns: [ | 
					
						
							|  |  |  |       { | 
					
						
							|  |  |  |         arg: 'messages', | 
					
						
							|  |  |  |         type: 'object', | 
					
						
							|  |  |  |         root: true | 
					
						
							|  |  |  |       } | 
					
						
							|  |  |  |     ] | 
					
						
							|  |  |  |   }); | 
					
						
							| 
									
										
										
										
											2015-06-11 19:11:07 -04:00
										 |  |  | }; |