diff --git a/api-server/common/models/User-Credential.js b/api-server/common/models/User-Credential.js index 2414ef8602..ccacd8c359 100644 --- a/api-server/common/models/User-Credential.js +++ b/api-server/common/models/User-Credential.js @@ -45,10 +45,22 @@ module.exports = function(UserCredential) { return findCred(query) .flatMap(_credentials => { const modified = new Date(); - const updateUser = User.update$( - { id: userId }, - createUserUpdatesFromProfile(provider, profile) - ); + const updateUser = new Promise((resolve, reject) => { + User.find({ id: userId }, (err, user) => { + if (err) { + return reject(err); + } + return user.updateAttributes( + createUserUpdatesFromProfile(provider, profile), + updateErr => { + if (updateErr) { + return reject(updateErr); + } + return resolve(); + } + ); + }); + }); let updateCredentials; if (!_credentials) { updateCredentials = createCred({ @@ -65,25 +77,18 @@ module.exports = function(UserCredential) { }); } else { _credentials.credentials = credentials; - updateCredentials = observeQuery( - _credentials, - 'updateAttributes', - { - profile: null, - credentials, - modified - } - ); + updateCredentials = observeQuery(_credentials, 'updateAttributes', { + profile: null, + credentials, + modified + }); } return Observable.combineLatest( - updateUser, + Observable.fromPromise(updateUser), updateCredentials, (_, credentials) => credentials ); }) - .subscribe( - credentials => cb(null, credentials), - cb - ); + .subscribe(credentials => cb(null, credentials), cb); }; }; diff --git a/api-server/common/models/User-Identity.js b/api-server/common/models/User-Identity.js index f9756885ca..bff8f3e6a5 100644 --- a/api-server/common/models/User-Identity.js +++ b/api-server/common/models/User-Identity.js @@ -126,16 +126,26 @@ export default function(UserIdent) { created: new Date(), ttl: user.constructor.settings.ttl }); - const updateUser = user.update$({ - email: email, - emailVerified: true, - emailAuthLinkTTL: null, - emailVerifyTTL: null - }); + const updateUser = new Promise((resolve, reject) => + user.updateAttributes( + { + email: email, + emailVerified: true, + emailAuthLinkTTL: null, + emailVerifyTTL: null + }, + err => { + if (err) { + return reject(err); + } + return resolve(); + } + ) + ); return Observable.combineLatest( Observable.of(user), createToken, - updateUser, + Observable.fromPromise(updateUser), (user, token) => ({ user, token }) ); }) diff --git a/api-server/common/models/user.js b/api-server/common/models/user.js index f3230fcf82..d024d83f02 100644 --- a/api-server/common/models/user.js +++ b/api-server/common/models/user.js @@ -20,13 +20,10 @@ import generate from 'nanoid/generate'; import { homeLocation, apiLocation } from '../../../config/env'; import { fixCompletedChallengeItem } from '../utils'; -import { themes } from '../utils/themes'; import { saveUser, observeMethod } from '../../server/utils/rx.js'; import { blacklistedUsernames } from '../../server/utils/constants.js'; import { wrapHandledError } from '../../server/utils/create-handled-error.js'; -import { - getEmailSender -} from '../../server/utils/url-utils.js'; +import { getEmailSender } from '../../server/utils/url-utils.js'; import { normaliseUserFields, getProgress, @@ -38,20 +35,15 @@ const BROWNIEPOINTS_TIMEOUT = [1, 'hour']; const nanoidCharSet = '0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ'; -const createEmailError = redirectTo => wrapHandledError( - new Error('email format is invalid'), - { +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 - } -); + }); function destroyAll(id, Model) { - return Observable.fromNodeCallback( - Model.destroyAll, - Model - )({ userId: id }); + return Observable.fromNodeCallback(Model.destroyAll, Model)({ userId: id }); } function buildCompletedChallengesUpdate(completedChallenges, project) { @@ -61,18 +53,16 @@ function buildCompletedChallengesUpdate(completedChallenges, project) { const currentCompletedChallenges = [ ...completedChallenges.map(fixCompletedChallengeItem) ]; - const currentCompletedProjects = currentCompletedChallenges - .filter(({id}) => solutionKeys.includes(id)); + const currentCompletedProjects = currentCompletedChallenges.filter(({ id }) => + solutionKeys.includes(id) + ); const now = Date.now(); const update = solutionKeys.reduce((update, currentId) => { - const indexOfCurrentId = _.findIndex( - update, - ({id}) => id === currentId - ); + const indexOfCurrentId = _.findIndex(update, ({ id }) => id === currentId); const isCurrentlyCompleted = indexOfCurrentId !== -1; if (isCurrentlyCompleted) { update[indexOfCurrentId] = { - ..._.find(update, ({id}) => id === currentId), + ..._.find(update, ({ id }) => id === currentId), solution: solutions[currentId] }; } @@ -90,16 +80,12 @@ function buildCompletedChallengesUpdate(completedChallenges, project) { return update; }, currentCompletedProjects); const updatedExisting = _.uniqBy( - [ - ...update, - ...currentCompletedChallenges - ], + [...update, ...currentCompletedChallenges], 'id' ); return { updated: updatedExisting, - isNewCompletionCount: - updatedExisting.length - completedChallenges.length + isNewCompletionCount: updatedExisting.length - completedChallenges.length }; } @@ -107,35 +93,41 @@ function isTheSame(val1, val2) { return val1 === val2; } -const renderSignUpEmail = loopback.template(path.join( - __dirname, - '..', - '..', - 'server', - 'views', - 'emails', - 'user-request-sign-up.ejs' -)); +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' -)); +const renderSignInEmail = loopback.template( + path.join( + __dirname, + '..', + '..', + 'server', + 'views', + 'emails', + 'user-request-sign-in.ejs' + ) +); -const renderEmailChangeEmail = loopback.template(path.join( - __dirname, - '..', - '..', - 'server', - 'views', - 'emails', - 'user-request-update-email.ejs' -)); +const renderEmailChangeEmail = loopback.template( + path.join( + __dirname, + '..', + '..', + 'server', + 'views', + 'emails', + 'user-request-update-email.ejs' + ) +); function getAboutProfile({ username, @@ -158,12 +150,12 @@ function nextTick(fn) { function getWaitPeriod(ttl) { const fiveMinutesAgo = moment().subtract(5, 'minutes'); const lastEmailSentAt = moment(new Date(ttl || null)); - const isWaitPeriodOver = ttl ? - lastEmailSentAt.isBefore(fiveMinutesAgo) : true; + const isWaitPeriodOver = ttl + ? lastEmailSentAt.isBefore(fiveMinutesAgo) + : true; if (!isWaitPeriodOver) { - const minutesLeft = 5 - - (moment().minutes() - lastEmailSentAt.minutes()); + const minutesLeft = 5 - (moment().minutes() - lastEmailSentAt.minutes()); return minutesLeft; } @@ -175,23 +167,22 @@ function getWaitMessage(ttl) { if (minutesLeft <= 0) { return null; } - const timeToWait = minutesLeft ? - `${minutesLeft} minute${minutesLeft > 1 ? 's' : ''}` : - 'a few seconds'; + const timeToWait = minutesLeft + ? `${minutesLeft} minute${minutesLeft > 1 ? 's' : ''}` + : 'a few seconds'; return dedent` Please wait ${timeToWait} to resend an authentication link. `; } +const getRandomNumber = () => Math.random(); module.exports = function(User) { // set salt factor for passwords User.settings.saltWorkFactor = 5; // set user.rand to random number - User.definition.rawProperties.rand.default = - User.definition.properties.rand.default = function() { - return Math.random(); - }; + User.definition.rawProperties.rand.default = getRandomNumber; + User.definition.properties.rand.default = getRandomNumber; // increase user accessToken ttl to 900 days User.settings.ttl = 900 * 24 * 60 * 60 * 1000; @@ -207,11 +198,8 @@ module.exports = function(User) { User.on('dataSourceAttached', () => { User.findOne$ = Observable.fromNodeCallback(User.findOne, User); - User.update$ = Observable.fromNodeCallback(User.updateAll, User); User.count$ = Observable.fromNodeCallback(User.count, User); - User.create$ = Observable.fromNodeCallback( - User.create.bind(User) - ); + User.create$ = Observable.fromNodeCallback(User.create.bind(User)); User.prototype.createAccessToken$ = Observable.fromNodeCallback( User.prototype.createAccessToken ); @@ -225,10 +213,7 @@ module.exports = function(User) { .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) - ) { + if (typeof user.email !== 'string' || !isEmail(user.email)) { throw createEmailError(); } // assign random username to new users @@ -250,21 +235,19 @@ module.exports = function(User) { if (user.progressTimestamps.length === 0) { user.progressTimestamps.push(Date.now()); } - return Observable.fromPromise(User.doesExist(null, user.email)) - .do(exists => { + return Observable.fromPromise(User.doesExist(null, user.email)).do( + exists => { if (exists) { - throw wrapHandledError( - new Error('user already exists'), - { - redirectTo: `${homeLocation}/signin`, - message: dedent` + throw wrapHandledError(new Error('user already exists'), { + redirectTo: `${homeLocation}/signin`, + message: dedent` The ${user.email} email address is already associated with an account. Try signing in with it here instead. ` - } - ); + }); } - }); + } + ); }) .ignoreElements(); @@ -282,9 +265,10 @@ module.exports = function(User) { } user.username = user.username.trim().toLowerCase(); - user.email = typeof user.email === 'string' ? - user.email.trim().toLowerCase() : - user.email; + user.email = + typeof user.email === 'string' + ? user.email.trim().toLowerCase() + : user.email; if (!user.progressTimestamps) { user.progressTimestamps = []; @@ -303,8 +287,7 @@ module.exports = function(User) { } }) .ignoreElements(); - return Observable.merge(beforeCreate, updateOrSave) - .toPromise(); + return Observable.merge(beforeCreate, updateOrSave).toPromise(); }); // remove lingering user identities before deleting user @@ -325,58 +308,60 @@ module.exports = function(User) { credData: credData }; } - ) - .subscribe( - function(data) { - log('deleted', data); - }, - function(err) { - log('error deleting user %s stuff', id, err); - next(err); - }, - function() { - log('user stuff deleted for user %s', id); - next(); - } - ); + ).subscribe( + function(data) { + log('deleted', data); + }, + function(err) { + log('error deleting user %s stuff', id, err); + next(err); + }, + function() { + log('user stuff deleted for user %s', id); + next(); + } + ); }); log('setting up user hooks'); // 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}`), - { - // standard oops - type: 'info', - redirectTo - } - ); - } - if (user.verificationToken !== token) { - throw wrapHandledError( - new Error(`Invalid token: ${token}`), - { - type: 'info', - message: dedent` + return this.findById(uid).then(user => { + if (!user) { + throw wrapHandledError(new Error(`User not found: ${uid}`), { + // standard oops + type: 'info', + redirectTo + }); + } + 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 + redirectTo + }); + } + return new Promise((resolve, reject) => + user.updateAttributes( + { + email: user.newEmail, + emailVerified: true, + emailVerifyTTL: null, + newEmail: null, + verificationToken: null + }, + err => { + if (err) { + return reject(err); } - ); - } - return user.update$({ - email: user.newEmail, - emailVerified: true, - emailVerifyTTL: null, - newEmail: null, - verificationToken: null - }).toPromise(); - }); + return resolve(); + } + ) + ); + }); }; function manualReload() { @@ -392,24 +377,21 @@ module.exports = function(User) { User.prototype.loginByRequest = function loginByRequest(req, res) { const { - query: { - emailChange - } + query: { emailChange } } = req; - const createToken = this.createAccessToken$() - .do(accessToken => { - const config = { - signed: !!req.signedCookies, - maxAge: accessToken.ttl, - domain: process.env.COOKIE_DOMAIN || 'localhost' - }; - if (accessToken && accessToken.id) { - const jwtAccess = jwt.sign({accessToken}, process.env.JWT_SECRET); - res.cookie('jwt_access_token', jwtAccess, config); - res.cookie('access_token', accessToken.id, config); - res.cookie('userId', accessToken.userId, config); - } - }); + const createToken = this.createAccessToken$().do(accessToken => { + const config = { + signed: !!req.signedCookies, + maxAge: accessToken.ttl, + domain: process.env.COOKIE_DOMAIN || 'localhost' + }; + if (accessToken && accessToken.id) { + const jwtAccess = jwt.sign({ accessToken }, process.env.JWT_SECRET); + res.cookie('jwt_access_token', jwtAccess, config); + res.cookie('access_token', accessToken.id, config); + res.cookie('userId', accessToken.userId, config); + } + }); let data = { emailVerified: true, emailAuthLinkTTL: null, @@ -422,16 +404,23 @@ module.exports = function(User) { newEmail: null }; } - const updateUser = this.update$(data); + const updateUser = new Promise((resolve, reject) => + this.updateAttributes(data, err => { + if (err) { + return reject(err); + } + return resolve(); + }) + ); return Observable.combineLatest( createToken, - updateUser, + Observable.fromPromise(updateUser), req.logIn(this), - (accessToken) => accessToken, + accessToken => accessToken ); }; - User.afterRemote('logout', function({req, res}, result, next) { + User.afterRemote('logout', function({ req, res }, result, next) { const config = { signed: !!req.signedCookies, domain: process.env.COOKIE_DOMAIN || 'localhost' @@ -461,36 +450,32 @@ module.exports = function(User) { where.email = email ? email.toLowerCase() : email; } log('where', where); - return User.count(where) - .then(count => count > 0); + return User.count(where).then(count => count > 0); }; - 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' + 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' } - ); + }); User.about = function about(username, cb) { if (!username) { @@ -511,28 +496,25 @@ module.exports = function(User) { }); }; - User.remoteMethod( - 'about', - { - description: 'get public info about user', - accepts: [ - { - arg: 'username', - type: 'string' - } - ], - returns: [ - { - arg: 'about', - type: 'object' - } - ], - http: { - path: '/about', - verb: 'get' + User.remoteMethod('about', { + description: 'get public info about user', + accepts: [ + { + arg: 'username', + type: 'string' } + ], + returns: [ + { + arg: 'about', + type: 'object' + } + ], + http: { + path: '/about', + verb: 'get' } - ); + }); User.prototype.createAuthToken = function createAuthToken({ ttl } = {}) { return Observable.fromNodeCallback( @@ -543,17 +525,12 @@ module.exports = function(User) { 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 - } + )(donation).do(() => + this.updateAttributes({ + isDonating: true, + donationEmails: [...(this.donationEmails || []), donation.email] }) - ) - .do(() => this.manualReload()); + ); }; User.prototype.getEncodedEmail = function getEncodedEmail(email) { @@ -569,13 +546,10 @@ module.exports = function(User) { return Observable.defer(() => { const messageOrNull = getWaitMessage(this.emailAuthLinkTTL); if (messageOrNull) { - throw wrapHandledError( - new Error('request is throttled'), - { - type: 'info', - message: messageOrNull - } - ); + throw wrapHandledError(new Error('request is throttled'), { + type: 'info', + message: messageOrNull + }); } // create a temporary access token with ttl for 15 minutes @@ -609,20 +583,29 @@ module.exports = function(User) { emailChange: !!newEmail }) }; + const userUpdate = new Promise((resolve, reject) => + this.updateAttributes({ emailAuthLinkTTL }, err => { + if (err) { + return reject(err); + } + return resolve(); + }) + ); return Observable.forkJoin( User.email.send$(mailOptions), - this.update$({ emailAuthLinkTTL }) + Observable.fromPromise(userUpdate) ); }) - .map(() => 'Check your email and click the link we sent you to confirm' + - ' your new email address.' + .map( + () => + 'Check your email and click the link we sent you to confirm' + + ' your new email address.' ); } User.prototype.requestAuthEmail = requestAuthEmail; User.prototype.requestUpdateEmail = function requestUpdateEmail(newEmail) { - const currentEmail = this.email; const isOwnEmail = isTheSame(newEmail, currentEmail); const isResendUpdateToSameEmail = isTheSame(newEmail, this.newEmail); @@ -631,28 +614,22 @@ module.exports = function(User) { if (isOwnEmail && isVerifiedEmail) { // email is already associated and verified with this account - throw wrapHandledError( - new Error('email is already verified'), - { - type: 'info', - message: ` + 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.` - } - ); + }); } if (isResendUpdateToSameEmail && isLinkSentWithinLimit) { // trying to update with the same newEmail and // confirmation email is still valid - throw wrapHandledError( - new Error(), - { - type: 'info', - message: dedent` + throw wrapHandledError(new Error(), { + type: 'info', + message: dedent` We have already sent an email confirmation request to ${newEmail}. ${isLinkSentWithinLimit}` - } - ); + }); } if (!isEmail('' + newEmail)) { throw createEmailError(); @@ -662,10 +639,10 @@ module.exports = function(User) { // this attempt to change email is the first or // previous attempts have expired if ( - !isOwnEmail || - (isOwnEmail && !isVerifiedEmail) || - (isResendUpdateToSameEmail && !isLinkSentWithinLimit) - ) { + !isOwnEmail || + (isOwnEmail && !isVerifiedEmail) || + (isResendUpdateToSameEmail && !isLinkSentWithinLimit) + ) { const updateConfig = { newEmail, emailVerified: false, @@ -674,35 +651,31 @@ module.exports = function(User) { // 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'), - { + .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(()=>{ - const updatePromise = new Promise((resolve, reject) => - this.updateAttributes(updateConfig, err => { - if (err) { - return reject(err); + message: `${newEmail} is already associated with another account.` + }); } - return resolve(); - })); - return Observable.forkJoin( - Observable.fromPromise(updatePromise), - this.requestAuthEmail(false, newEmail), - (_, message) => message - ); - }); - + }) + .flatMap(() => { + const updatePromise = new Promise((resolve, reject) => + this.updateAttributes(updateConfig, err => { + if (err) { + return reject(err); + } + return resolve(); + }) + ); + return Observable.forkJoin( + Observable.fromPromise(updatePromise), + this.requestAuthEmail(false, newEmail), + (_, message) => message + ); + }); } else { return 'Something unexpected happened whilst updating your email.'; } @@ -714,62 +687,77 @@ module.exports = function(User) { User.prototype.requestCompletedChallenges = requestCompletedChallenges; - User.prototype.requestUpdateFlags = function requestUpdateFlags(values) { + User.prototype.requestUpdateFlags = async 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` + const flagsToUpdate = flagsToCheck.filter( + flag => !isTheSame(values[flag], valuesToCheck[flag]) + ); + if (!flagsToUpdate.length) { + return Observable.of( + dedent` No property in ${JSON.stringify(flagsToCheck, null, 2)} will introduce a change in this user. ` - ) - .map(() => dedent`Your settings have not been updated.`); + ).map(() => dedent`Your settings have not been updated.`); } - 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)) - .flatMap(() => this.update$({ [flag]: newValue })); - }) - ); + const userUpdateData = flagsToUpdate.reduce((data, currentFlag) => { + data[currentFlag] = values[currentFlag]; + return data; + }, {}); + log(userUpdateData); + const userUpdate = new Promise((resolve, reject) => + this.updateAttributes(userUpdateData, err => { + if (err) { + return reject(err); + } + return resolve(); }) - .doOnNext(() => this.manualReload()) - .map(() => dedent` + ); + return Observable.fromPromise(userUpdate).map( + () => dedent` We have successfully updated your account. - `); + ` + ); }; - User.prototype.updateMyPortfolio = - function updateMyPortfolio(portfolioItem, deleteRequest) { - const currentPortfolio = this.portfolio.slice(0); - const pIndex = _.findIndex( - currentPortfolio, - p => p.id === portfolioItem.id + 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 ); - 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 }) - .do(() => this.manualReload()) - .map(() => dedent` + } else if (pIndex === -1) { + updatedPortfolio = currentPortfolio.concat([portfolioItem]); + } else { + updatedPortfolio = [...currentPortfolio]; + updatedPortfolio[pIndex] = { ...portfolioItem }; + } + const userUpdate = new Promise((resolve, reject) => + this.updateAttribute('portfolio', updatedPortfolio, err => { + if (err) { + return reject(err); + } + return resolve(); + }) + ); + return Observable.fromPromise(userUpdate).map( + () => dedent` Your portfolio has been updated. - `); - }; + ` + ); + }; User.prototype.updateMyProjects = function updateMyProjects(project) { const updateData = { $set: {} }; @@ -778,10 +766,7 @@ module.exports = function(User) { const { updated, isNewCompletionCount - } = buildCompletedChallengesUpdate( - this.completedChallenges, - project - ); + } = buildCompletedChallengesUpdate(this.completedChallenges, project); updateData.$set.completedChallenges = updated; if (isNewCompletionCount) { let points = []; @@ -792,43 +777,53 @@ module.exports = function(User) { $each: points.map(() => Date.now()) }; } - return this.update$(updateData); + const updatePromise = new Promise((resolve, reject) => + this.updateAttributes(updateData, err => { + if (err) { + return reject(err); + } + return resolve(); + }) + ); + return Observable.fromPromise(updatePromise); }) - .doOnNext(() => this.manualReload() ) - .map(() => dedent` + .map( + () => dedent` Your projects have been updated. - `); + ` + ); }; User.prototype.updateMyProfileUI = function updateMyProfileUI(profileUI) { - const oldUI = { ...this.profileUI }; - const update = { - profileUI: { - ...oldUI, - ...profileUI - } + const newProfileUI = { + ...this.profileUI, + ...profileUI }; - - return this.update$(update) - .doOnNext(() => this.manualReload()) - .map(() => dedent` + const profileUIUpdate = new Promise((resolve, reject) => + this.updateAttribute('profileUI', newProfileUI, err => { + if (err) { + return reject(err); + } + return resolve(); + }) + ); + return Observable.fromPromise(profileUIUpdate).map( + () => dedent` Your privacy settings have been updated. - `); + ` + ); }; User.prototype.updateMyUsername = function updateMyUsername(newUsername) { - return Observable.defer( - () => { - const isOwnUsername = isTheSame(newUsername, this.username); - if (isOwnUsername) { - return Observable.of(dedent` + return Observable.defer(() => { + const isOwnUsername = isTheSame(newUsername, this.username); + if (isOwnUsername) { + return Observable.of(dedent` ${newUsername} is already associated with this account. `); - } - return Observable.fromPromise(User.doesExist(newUsername)); } - ) - .flatMap(boolOrMessage => { + return Observable.fromPromise(User.doesExist(newUsername)); + }).flatMap(boolOrMessage => { if (typeof boolOrMessage === 'string') { return Observable.of(boolOrMessage); } @@ -838,11 +833,20 @@ module.exports = function(User) { `); } - return this.update$({ username: newUsername }) - .do(() => this.manualReload()) - .map(() => dedent` + const usernameUpdate = new Promise((resolve, reject) => + this.updateAttribute('username', newUsername, err => { + if (err) { + return reject(err); + } + return resolve(); + }) + ); + + return Observable.fromPromise(usernameUpdate).map( + () => dedent` Your username has been updated successfully. - `); + ` + ); }); }; @@ -896,7 +900,7 @@ module.exports = function(User) { } User.getPublicProfile = function getPublicProfile(username, cb) { - return User.findOne$({ where: { username }}) + return User.findOne$({ where: { username } }) .flatMap(user => { if (!user) { return Observable.of({}); @@ -932,10 +936,7 @@ module.exports = function(User) { result: user.username }); }) - .subscribe( - user => cb(null, user), - cb - ); + .subscribe(user => cb(null, user), cb); }; User.remoteMethod('getPublicProfile', { @@ -957,53 +958,56 @@ module.exports = function(User) { } }); - User.giveBrowniePoints = - function giveBrowniePoints(receiver, giver, data = {}, dev = false, cb) { - const findUser = observeMethod(User, 'findOne'); - if (!receiver) { - return nextTick(() => { - cb( - new TypeError(`receiver should be a string but got ${ receiver }`) - ); - }); - } - if (!giver) { - return nextTick(() => { - cb(new TypeError(`giver should be a string but got ${ giver }`)); - }); - } - let temp = moment(); - const browniePoints = temp - .subtract.apply(temp, BROWNIEPOINTS_TIMEOUT) - .valueOf(); - const user$ = findUser({ where: { username: receiver }}); + User.giveBrowniePoints = function giveBrowniePoints( + receiver, + giver, + data = {}, + dev = false, + cb + ) { + const findUser = observeMethod(User, 'findOne'); + if (!receiver) { + return nextTick(() => { + cb(new TypeError(`receiver should be a string but got ${receiver}`)); + }); + } + if (!giver) { + return nextTick(() => { + cb(new TypeError(`giver should be a string but got ${giver}`)); + }); + } + let temp = moment(); + const browniePoints = temp.subtract + .apply(temp, BROWNIEPOINTS_TIMEOUT) + .valueOf(); + const user$ = findUser({ where: { username: receiver } }); - return user$ - .tapOnNext((user) => { + return ( + user$ + .tapOnNext(user => { if (!user) { - throw new Error(`could not find receiver for ${ receiver }`); + throw new Error(`could not find receiver for ${receiver}`); } }) .flatMap(({ progressTimestamps = [] }) => { return Observable.from(progressTimestamps); }) // filter out non objects - .filter((timestamp) => !!timestamp || typeof timestamp === 'object') + .filter(timestamp => !!timestamp || typeof timestamp === 'object') // filterout timestamps older then an hour .filter(({ timestamp = 0 }) => { return timestamp >= browniePoints; }) // filter out brownie points given by giver - .filter((browniePoint) => { + .filter(browniePoint => { return browniePoint.giver === giver; }) // no results means this is the first brownie point given by giver // so return -1 to indicate receiver should receive point .first({ defaultValue: -1 }) - .flatMap((browniePointsFromGiver) => { + .flatMap(browniePointsFromGiver => { if (browniePointsFromGiver === -1) { - - return user$.flatMap((user) => { + return user$.flatMap(user => { user.progressTimestamps.push({ giver, timestamp: Date.now(), @@ -1013,139 +1017,73 @@ module.exports = function(User) { }); } return Observable.throw( - new Error(`${ giver } already gave ${ receiver } points`) + new Error(`${giver} already gave ${receiver} points`) ); }) .subscribe( - (user) => { + user => { return cb( null, getAboutProfile(user), - dev ? - { giver, receiver, data } : - null + dev ? { giver, receiver, data } : null ); }, - (e) => cb(e, null, dev ? { giver, receiver, data } : null), + e => cb(e, null, dev ? { giver, receiver, data } : null), () => { log('brownie points assigned completed'); } - ); - }; - - 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' - }, - { - arg: 'debug', - type: 'boolean' - } - ], - returns: [ - { - arg: 'about', - type: 'object' - }, - { - arg: 'debug', - type: 'object' - } - ], - http: { - path: '/give-brownie-points', - verb: 'POST' - } - } - ); - - User.themes = themes; - - User.prototype.updateTheme = function updateTheme(theme) { - if (!this.constructor.themes[theme]) { - const err = wrapHandledError( - new Error('Theme is not valid.'), - { - Type: 'info', - message: err.message - } - ); - return Promise.reject(err); - } - return this.update$({ theme }) - .doOnNext(() => this.manualReload()) - .toPromise(); + ) + ); }; - // deprecated. remove once live - 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' + 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' + }, + { + arg: 'debug', + type: 'boolean' } + ], + returns: [ + { + arg: 'about', + type: 'object' + }, + { + arg: 'debug', + type: 'object' + } + ], + http: { + path: '/give-brownie-points', + verb: 'POST' } - ); + }); - // user.updateTo$(updateData: Object) => Observable[Number] - User.prototype.update$ = function update$(updateData) { - const id = this.getId(); - const updateOptions = { allowExtendedOperators: true }; - if ( - !updateData || - typeof updateData !== 'object' || - !Object.keys(updateData).length - ) { - return Observable.throw(new Error( - dedent` - updateData must be an object with at least one key, - but got ${updateData} with ${Object.keys(updateData).length} - `.split('\n').join(' ') - )); - } - return this.constructor.update$({ id }, updateData, updateOptions); - }; User.prototype.getPoints$ = function getPoints$() { 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; - }); + return this.constructor.findOne$(filter).map(user => { + this.progressTimestamps = user.progressTimestamps; + return user.progressTimestamps; + }); }; User.prototype.getCompletedChallenges$ = function getCompletedChallenges$() { const id = this.getId(); @@ -1153,11 +1091,10 @@ module.exports = function(User) { where: { id }, fields: { completedChallenges: true } }; - return this.constructor.findOne$(filter) - .map(user => { - this.completedChallenges = user.completedChallenges; - return user.completedChallenges; - }); + return this.constructor.findOne$(filter).map(user => { + this.completedChallenges = user.completedChallenges; + return user.completedChallenges; + }); }; User.getMessages = messages => Promise.resolve(messages); diff --git a/api-server/server/boot/certificate.js b/api-server/server/boot/certificate.js index 22f71cdff3..debe581468 100644 --- a/api-server/server/boot/certificate.js +++ b/api-server/server/boot/certificate.js @@ -272,9 +272,17 @@ function createVerifyCert(certTypeIds, app) { // set here so sendCertifiedEmail works properly // not used otherwise user[certType] = true; + const updatePromise = new Promise((resolve, reject) => + user.updateAttributes(updateData, err => { + if (err) { + return reject(err); + } + return resolve(); + }) + ); return Observable.combineLatest( // update user data - user.update$(updateData), + Observable.fromPromise(updatePromise), // If user has committed to nonprofit, // this will complete their pledge completeCommitment$(user), diff --git a/api-server/server/boot/challenge.js b/api-server/server/boot/challenge.js index 6e3ab0071a..03d7a15010 100644 --- a/api-server/server/boot/challenge.js +++ b/api-server/server/boot/challenge.js @@ -4,7 +4,7 @@ * a db migration to fix all completedChallenges * */ - +import { Observable } from 'rx'; import _ from 'lodash'; import debug from 'debug'; import accepts from 'accepts'; @@ -87,9 +87,6 @@ function buildUserUpdate(user, challengeId, _completedChallenge, timezone) { timezone: userTimezone }; } - - log('user update data', updateData); - return { alreadyCompleted, updateData, @@ -215,21 +212,24 @@ export default async function bootChallenge(app, done) { }); const points = alreadyCompleted ? user.points : user.points + 1; - - return user - .update$(updateData) - .doOnNext(() => user.manualReload()) - .doOnNext(({ count }) => log('%s documents updated', count)) - .map(() => { - if (type === 'json') { - return res.json({ - points, - alreadyCompleted, - completedDate - }); + const updatePromise = new Promise((resolve, reject) => + user.updateAttributes(updateData, err => { + if (err) { + return reject(err); } - return res.sendStatus(200); - }); + return resolve(); + }) + ); + return Observable.fromPromise(updatePromise).map(() => { + if (type === 'json') { + return res.json({ + points, + alreadyCompleted, + completedDate + }); + } + return res.sendStatus(200); + }); }) .subscribe(() => {}, next); } @@ -239,6 +239,8 @@ export default async function bootChallenge(app, done) { const type = accepts(req).type('html', 'json', 'text'); const errors = req.validationErrors(true); + const { user } = req; + if (errors) { if (type === 'json') { return res.status(403).send({ errors }); @@ -248,35 +250,39 @@ export default async function bootChallenge(app, done) { return res.sendStatus(403); } - return req.user + return user .getCompletedChallenges$() .flatMap(() => { const completedDate = Date.now(); const { id, solution, timezone, files } = req.body; const { alreadyCompleted, updateData } = buildUserUpdate( - req.user, + user, id, { id, solution, completedDate, files }, timezone ); - const user = req.user; const points = alreadyCompleted ? user.points : user.points + 1; - return user - .update$(updateData) - .doOnNext(({ count }) => log('%s documents updated', count)) - .map(() => { - if (type === 'json') { - return res.json({ - points, - alreadyCompleted, - completedDate - }); + const updatePromise = new Promise((resolve, reject) => + user.updateAttributes(updateData, err => { + if (err) { + return reject(err); } - return res.sendStatus(200); - }); + return resolve(); + }) + ); + return Observable.fromPromise(updatePromise).map(() => { + if (type === 'json') { + return res.json({ + points, + alreadyCompleted, + completedDate + }); + } + return res.sendStatus(200); + }); }) .subscribe(() => {}, next); } @@ -329,20 +335,24 @@ export default async function bootChallenge(app, done) { completedChallenge ); - return user - .update$(updateData) - .doOnNext(() => user.manualReload()) - .doOnNext(({ count }) => log('%s documents updated', count)) - .doOnNext(() => { - if (type === 'json') { - return res.send({ - alreadyCompleted, - points: alreadyCompleted ? user.points : user.points + 1, - completedDate: completedChallenge.completedDate - }); + const updatePromise = new Promise((resolve, reject) => + user.updateAttributes(updateData, err => { + if (err) { + return reject(err); } - return res.status(200).send(true); - }); + return resolve(); + }) + ); + return Observable.fromPromise(updatePromise).doOnNext(() => { + if (type === 'json') { + return res.send({ + alreadyCompleted, + points: alreadyCompleted ? user.points : user.points + 1, + completedDate: completedChallenge.completedDate + }); + } + return res.status(200).send(true); + }); }) .subscribe(() => {}, next); } @@ -376,19 +386,24 @@ export default async function bootChallenge(app, done) { completedChallenge ); - return user - .update$(updateData) - .doOnNext(({ count }) => log('%s documents updated', count)) - .doOnNext(() => { - if (type === 'json') { - return res.send({ - alreadyCompleted, - points: alreadyCompleted ? user.points : user.points + 1, - completedDate: completedChallenge.completedDate - }); + const updatePromise = new Promise((resolve, reject) => + user.updateAttributes(updateData, err => { + if (err) { + return reject(err); } - return res.status(200).send(true); - }); + return resolve(); + }) + ); + return Observable.fromPromise(updatePromise).doOnNext(() => { + if (type === 'json') { + return res.send({ + alreadyCompleted, + points: alreadyCompleted ? user.points : user.points + 1, + completedDate: completedChallenge.completedDate + }); + } + return res.status(200).send(true); + }); }) .subscribe(() => {}, next); } diff --git a/api-server/server/boot/user.js b/api-server/server/boot/user.js index 7950dbf372..d34110e2cc 100644 --- a/api-server/server/boot/user.js +++ b/api-server/server/boot/user.js @@ -120,12 +120,15 @@ function getUnlinkSocial(req, res, next) { const updateData = { [social]: null }; - return user.update$(updateData).subscribe(() => { + return user.updateAttributes(updateData, err => { + if (err) { + return next(err); + } log(`${social} has been unlinked successfully`); req.flash('info', `You've successfully unlinked your ${social}.`); - return res.redirect('/' + username); - }, next); + return res.redirectWithFlash(`${homeLocation}/${username}`); + }); }); }); } diff --git a/api-server/server/datasources.json b/api-server/server/datasources.json index 70084fe218..b3e3ac9cbb 100644 --- a/api-server/server/datasources.json +++ b/api-server/server/datasources.json @@ -1,7 +1,8 @@ { "db": { "name": "db", - "connector": "mongodb" + "connector": "mongodb", + "allowExtendedOperators": true }, "mail": { "name": "mail",