Permalink
Cannot retrieve contributors at this time
Fetching contributors…
| import { Observable } from 'rx'; | |
| import uuid from 'node-uuid'; | |
| import moment from 'moment'; | |
| import dedent from 'dedent'; | |
| import debugFactory from 'debug'; | |
| import { saveUser, observeMethod } from '../../server/utils/rx'; | |
| import { blacklistedUsernames } from '../../server/utils/constants'; | |
| const debug = debugFactory('fcc:user:remote'); | |
| const BROWNIEPOINTS_TIMEOUT = [1, 'hour']; | |
| function getAboutProfile({ | |
| username, | |
| githubProfile: github, | |
| progressTimestamps = [], | |
| bio | |
| }) { | |
| return { | |
| username, | |
| github, | |
| browniePoints: progressTimestamps.length, | |
| bio | |
| }; | |
| } | |
| function nextTick(fn) { | |
| return process.nextTick(fn); | |
| } | |
| module.exports = function(User) { | |
| // NOTE(berks): user email validation currently not needed but build in. This | |
| // work around should let us sneak by | |
| // see: | |
| // https://github.com/strongloop/loopback/issues/1137#issuecomment-109200135 | |
| delete User.validations.email; | |
| // 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(); | |
| }; | |
| // username should not be in blacklist | |
| User.validatesExclusionOf('username', { | |
| 'in': blacklistedUsernames, | |
| message: 'is taken' | |
| }); | |
| // username should be unique | |
| User.validatesUniquenessOf('username'); | |
| User.settings.emailVerificationRequired = false; | |
| 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.observe('before save', function({ instance: user }, next) { | |
| if (user) { | |
| 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) { | |
| user.progressTimestamps.push({ timestamp: Date.now() }); | |
| } | |
| } | |
| next(); | |
| }); | |
| debug('setting up user hooks'); | |
| User.afterRemote('confirm', function(ctx) { | |
| ctx.req.flash('success', { | |
| msg: [ | |
| 'You\'re email has been confirmed!' | |
| ] | |
| }); | |
| ctx.res.redirect('/email-signin'); | |
| }); | |
| User.beforeRemote('create', function({ req, res }, _, next) { | |
| req.body.username = 'fcc' + uuid.v4().slice(0, 8); | |
| if (!req.body.email) { | |
| return next(); | |
| } | |
| return User.doesExist(null, req.body.email) | |
| .then(exists => { | |
| if (!exists) { | |
| return next(); | |
| } | |
| req.flash('error', { | |
| msg: dedent` | |
| The ${req.body.email} email address is already associated with an account. | |
| Try signing in with it here instead. | |
| ` | |
| }); | |
| return res.redirect('/email-signin'); | |
| }) | |
| .catch(err => { | |
| console.error(err); | |
| req.flash('error', { | |
| msg: 'Oops, something went wrong, please try again later' | |
| }); | |
| return res.redirect('/email-signup'); | |
| }); | |
| }); | |
| User.on('resetPasswordRequest', function(info) { | |
| let url; | |
| const host = User.app.get('host'); | |
| const { id: token } = info.accessToken; | |
| if (process.env.NODE_ENV === 'development') { | |
| const port = User.app.get('port'); | |
| url = `http://${host}:${port}/reset-password?access_token=${token}`; | |
| } else { | |
| url = | |
| `http://freecodecamp.com/reset-password?access_token=${token}`; | |
| } | |
| // the email of the requested user | |
| debug(info.email); | |
| // the temp access token to allow password reset | |
| debug(info.accessToken.id); | |
| // requires AccessToken.belongsTo(User) | |
| var mailOptions = { | |
| to: info.email, | |
| from: 'Team@freecodecamp.com', | |
| subject: 'Password Reset Request', | |
| text: ` | |
| Hello,\n\n | |
| This email is confirming that you requested to | |
| reset your password for your Free Code Camp account. | |
| This is your email: ${ info.email }. | |
| Go to ${ url } to reset your password. | |
| \n | |
| Happy Coding! | |
| \n | |
| ` | |
| }; | |
| User.app.models.Email.send(mailOptions, function(err) { | |
| if (err) { console.error(err); } | |
| debug('email reset sent'); | |
| }); | |
| }); | |
| User.beforeRemote('login', function(ctx, notUsed, next) { | |
| const { body } = ctx.req; | |
| if (body && typeof body.email === 'string') { | |
| body.email = body.email.toLowerCase(); | |
| } | |
| next(); | |
| }); | |
| User.afterRemote('login', function(ctx, accessToken, next) { | |
| var res = ctx.res; | |
| var req = ctx.req; | |
| // var args = ctx.args; | |
| var config = { | |
| signed: !!req.signedCookies, | |
| maxAge: accessToken.ttl | |
| }; | |
| if (accessToken && accessToken.id) { | |
| debug('setting cookies'); | |
| res.cookie('access_token', accessToken.id, config); | |
| res.cookie('userId', accessToken.userId, config); | |
| } | |
| return req.logIn({ id: accessToken.userId.toString() }, function(err) { | |
| if (err) { return next(err); } | |
| debug('user logged in'); | |
| if (req.session && req.session.returnTo) { | |
| var redirectTo = req.session.returnTo; | |
| if (redirectTo === '/map-aside') { | |
| redirectTo = '/map'; | |
| } | |
| return res.redirect(redirectTo); | |
| } | |
| req.flash('success', { msg: 'Success! You are logged in.' }); | |
| return res.redirect('/'); | |
| }); | |
| }); | |
| User.afterRemoteError('login', function(ctx) { | |
| var res = ctx.res; | |
| var req = ctx.req; | |
| req.flash('errors', { | |
| msg: 'Invalid username or password.' | |
| }); | |
| return res.redirect('/email-signin'); | |
| }); | |
| User.afterRemote('logout', function(ctx, result, next) { | |
| var res = ctx.res; | |
| res.clearCookie('access_token'); | |
| res.clearCookie('userId'); | |
| next(); | |
| }); | |
| User.doesExist = function doesExist(username, email) { | |
| if (!username && !email) { | |
| return Promise.resolve(false); | |
| } | |
| debug('checking existence'); | |
| // check to see if username is on blacklist | |
| if (username && blacklistedUsernames.indexOf(username) !== -1) { | |
| return Promise.resolve(true); | |
| } | |
| var where = {}; | |
| if (username) { | |
| where.username = username.toLowerCase(); | |
| } else { | |
| where.email = email ? email.toLowerCase() : email; | |
| } | |
| debug('where', where); | |
| 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.about = function about(username, cb) { | |
| if (!username) { | |
| // Zalgo!! | |
| return nextTick(() => { | |
| cb(new TypeError( | |
| `username should be a string but got ${ username }` | |
| )); | |
| }); | |
| } | |
| User.findOne({ where: { username } }, (err, user) => { | |
| if (err) { | |
| return cb(err); | |
| } | |
| if (!user || user.username !== username) { | |
| return cb(new Error(`no user found for ${ username }`)); | |
| } | |
| const aboutUser = getAboutProfile(user); | |
| return cb(null, aboutUser); | |
| }); | |
| }; | |
| 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.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$ | |
| .tapOnNext((user) => { | |
| if (!user) { | |
| 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') | |
| // filterout timestamps older then an hour | |
| .filter(({ timestamp = 0 }) => { | |
| return timestamp >= browniePoints; | |
| }) | |
| // filter out brownie points given by giver | |
| .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) => { | |
| if (browniePointsFromGiver === -1) { | |
| return user$.flatMap((user) => { | |
| user.progressTimestamps.push({ | |
| giver, | |
| timestamp: Date.now(), | |
| ...data | |
| }); | |
| return saveUser(user); | |
| }); | |
| } | |
| return Observable.throw( | |
| new Error(`${ giver } already gave ${ receiver } points`) | |
| ); | |
| }) | |
| .subscribe( | |
| (user) => { | |
| return cb( | |
| null, | |
| getAboutProfile(user), | |
| dev ? | |
| { giver, receiver, data } : | |
| null | |
| ); | |
| }, | |
| (e) => cb(e, null, dev ? { giver, receiver, data } : null), | |
| () => { | |
| debug('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.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); | |
| }; | |
| }; |