442 lines
		
	
	
		
			12 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
			
		
		
	
	
			442 lines
		
	
	
		
			12 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
| /**
 | |
|  *
 | |
|  * Any ref to fixCompletedChallengesItem should be removed post
 | |
|  * a db migration to fix all completedChallenges
 | |
|  *
 | |
|  */
 | |
| import { Observable } from 'rx';
 | |
| import _ from 'lodash';
 | |
| import debug from 'debug';
 | |
| import accepts from 'accepts';
 | |
| import dedent from 'dedent';
 | |
| 
 | |
| import { homeLocation } from '../../../config/env.json';
 | |
| 
 | |
| import { ifNoUserSend } from '../utils/middleware';
 | |
| import { dasherize } from '../utils';
 | |
| import pathMigrations from '../resources/pathMigration.json';
 | |
| import { fixCompletedChallengeItem } from '../../common/utils';
 | |
| 
 | |
| const log = debug('fcc:boot:challenges');
 | |
| 
 | |
| const learnURL = `${homeLocation}/learn`;
 | |
| 
 | |
| const jsProjects = [
 | |
|   'aaa48de84e1ecc7c742e1124',
 | |
|   'a7f4d8f2483413a6ce226cac',
 | |
|   '56533eb9ac21ba0edf2244e2',
 | |
|   'aff0395860f5d3034dc0bfc9',
 | |
|   'aa2e6f85cab2ab736c9a9b24'
 | |
| ];
 | |
| 
 | |
| function buildUserUpdate(user, challengeId, _completedChallenge, timezone) {
 | |
|   const { files } = _completedChallenge;
 | |
|   let completedChallenge = {};
 | |
| 
 | |
|   if (jsProjects.includes(challengeId)) {
 | |
|     completedChallenge = {
 | |
|       ..._completedChallenge,
 | |
|       files: Object.keys(files)
 | |
|         .map(key => files[key])
 | |
|         .map(file =>
 | |
|           _.pick(file, ['contents', 'key', 'index', 'name', 'path', 'ext'])
 | |
|         )
 | |
|     };
 | |
|   } else {
 | |
|     completedChallenge = _.omit(_completedChallenge, ['files']);
 | |
|   }
 | |
|   let finalChallenge;
 | |
|   const updateData = {};
 | |
|   const { timezone: userTimezone, completedChallenges = [] } = user;
 | |
| 
 | |
|   const oldChallenge = _.find(
 | |
|     completedChallenges,
 | |
|     ({ id }) => challengeId === id
 | |
|   );
 | |
|   const alreadyCompleted = !!oldChallenge;
 | |
| 
 | |
|   if (alreadyCompleted) {
 | |
|     finalChallenge = {
 | |
|       ...completedChallenge,
 | |
|       completedDate: oldChallenge.completedDate
 | |
|     };
 | |
|   } else {
 | |
|     updateData.$push = {
 | |
|       ...updateData.$push,
 | |
|       progressTimestamps: Date.now()
 | |
|     };
 | |
|     finalChallenge = {
 | |
|       ...completedChallenge
 | |
|     };
 | |
|   }
 | |
| 
 | |
|   updateData.$set = {
 | |
|     completedChallenges: _.uniqBy(
 | |
|       [finalChallenge, ...completedChallenges.map(fixCompletedChallengeItem)],
 | |
|       'id'
 | |
|     )
 | |
|   };
 | |
| 
 | |
|   if (
 | |
|     timezone &&
 | |
|     timezone !== 'UTC' &&
 | |
|     (!userTimezone || userTimezone === 'UTC')
 | |
|   ) {
 | |
|     updateData.$set = {
 | |
|       ...updateData.$set,
 | |
|       timezone: userTimezone
 | |
|     };
 | |
|   }
 | |
|   return {
 | |
|     alreadyCompleted,
 | |
|     updateData,
 | |
|     completedDate: finalChallenge.completedDate
 | |
|   };
 | |
| }
 | |
| 
 | |
| function buildChallengeUrl(challenge) {
 | |
|   const { superBlock, block, dashedName } = challenge;
 | |
|   return `/learn/${dasherize(superBlock)}/${dasherize(block)}/${dashedName}`;
 | |
| }
 | |
| 
 | |
| function getFirstChallenge(Challenge) {
 | |
|   return new Promise(resolve => {
 | |
|     Challenge.find(
 | |
|       { where: { challengeOrder: 0, superOrder: 1, order: 0 } },
 | |
|       (err, challenge) => {
 | |
|         if (err) {
 | |
|           console.log(err);
 | |
|           return resolve('/learn');
 | |
|         }
 | |
|         return resolve(buildChallengeUrl(challenge));
 | |
|       }
 | |
|     );
 | |
|   });
 | |
| }
 | |
| 
 | |
| async function createChallengeUrlResolver(app) {
 | |
|   const { Challenge } = app.models;
 | |
|   const cache = new Map();
 | |
|   const firstChallenge = await getFirstChallenge(Challenge);
 | |
| 
 | |
|   return function resolveChallengeUrl(id) {
 | |
|     return new Promise(resolve => {
 | |
|       if (cache.has(id)) {
 | |
|         return resolve(cache.get(id));
 | |
|       }
 | |
|       return Challenge.findById(id, (err, challenge) => {
 | |
|         if (err) {
 | |
|           console.log(err);
 | |
|           return firstChallenge;
 | |
|         }
 | |
|         const challengeUrl = buildChallengeUrl(challenge);
 | |
|         cache.set(id, challengeUrl);
 | |
|         return resolve(challengeUrl);
 | |
|       });
 | |
|     });
 | |
|   };
 | |
| }
 | |
| 
 | |
| export default async function bootChallenge(app, done) {
 | |
|   const send200toNonUser = ifNoUserSend(true);
 | |
|   const api = app.loopback.Router();
 | |
|   const router = app.loopback.Router();
 | |
|   const challengeUrlResolver = await createChallengeUrlResolver(app);
 | |
| 
 | |
|   api.post(
 | |
|     '/modern-challenge-completed',
 | |
|     send200toNonUser,
 | |
|     modernChallengeCompleted
 | |
|   );
 | |
| 
 | |
|   // deprecate endpoint
 | |
|   // remove once new endpoint is live
 | |
|   api.post('/completed-challenge', send200toNonUser, completedChallenge);
 | |
| 
 | |
|   api.post('/challenge-completed', send200toNonUser, completedChallenge);
 | |
| 
 | |
|   // deprecate endpoint
 | |
|   // remove once new endpoint is live
 | |
|   api.post(
 | |
|     '/completed-zipline-or-basejump',
 | |
|     send200toNonUser,
 | |
|     projectCompleted
 | |
|   );
 | |
| 
 | |
|   api.post('/project-completed', send200toNonUser, projectCompleted);
 | |
| 
 | |
|   api.post(
 | |
|     '/backend-challenge-completed',
 | |
|     send200toNonUser,
 | |
|     backendChallengeCompleted
 | |
|   );
 | |
| 
 | |
|   router.get('/challenges/current-challenge', redirectToCurrentChallenge);
 | |
| 
 | |
|   router.get('/challenges', redirectToLearn);
 | |
| 
 | |
|   router.get('/challenges/*', redirectToLearn);
 | |
| 
 | |
|   router.get('/map', redirectToLearn);
 | |
| 
 | |
|   app.use(api);
 | |
|   app.use('/external', api);
 | |
|   app.use('/internal', api);
 | |
|   app.use(router);
 | |
| 
 | |
|   function modernChallengeCompleted(req, res, next) {
 | |
|     const type = accepts(req).type('html', 'json', 'text');
 | |
|     req.checkBody('id', 'id must be an ObjectId').isMongoId();
 | |
| 
 | |
|     const errors = req.validationErrors(true);
 | |
|     if (errors) {
 | |
|       if (type === 'json') {
 | |
|         return res.status(403).send({ errors });
 | |
|       }
 | |
| 
 | |
|       log('errors', errors);
 | |
|       return res.sendStatus(403);
 | |
|     }
 | |
| 
 | |
|     const user = req.user;
 | |
|     return user
 | |
|       .getCompletedChallenges$()
 | |
|       .flatMap(() => {
 | |
|         const completedDate = Date.now();
 | |
|         const { id, files } = req.body;
 | |
| 
 | |
|         const { alreadyCompleted, updateData } = buildUserUpdate(user, id, {
 | |
|           id,
 | |
|           files,
 | |
|           completedDate
 | |
|         });
 | |
| 
 | |
|         const points = alreadyCompleted ? user.points : user.points + 1;
 | |
|         const updatePromise = new Promise((resolve, reject) =>
 | |
|           user.updateAttributes(updateData, err => {
 | |
|             if (err) {
 | |
|               return reject(err);
 | |
|             }
 | |
|             return resolve();
 | |
|           })
 | |
|         );
 | |
|         return Observable.fromPromise(updatePromise).map(() => {
 | |
|           if (type === 'json') {
 | |
|             return res.json({
 | |
|               points,
 | |
|               alreadyCompleted,
 | |
|               completedDate
 | |
|             });
 | |
|           }
 | |
|           return res.sendStatus(200);
 | |
|         });
 | |
|       })
 | |
|       .subscribe(() => {}, next);
 | |
|   }
 | |
| 
 | |
|   function completedChallenge(req, res, next) {
 | |
|     req.checkBody('id', 'id must be an ObjectId').isMongoId();
 | |
|     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 });
 | |
|       }
 | |
| 
 | |
|       log('errors', errors);
 | |
|       return res.sendStatus(403);
 | |
|     }
 | |
| 
 | |
|     return user
 | |
|       .getCompletedChallenges$()
 | |
|       .flatMap(() => {
 | |
|         const completedDate = Date.now();
 | |
|         const { id, solution, timezone, files } = req.body;
 | |
| 
 | |
|         const { alreadyCompleted, updateData } = buildUserUpdate(
 | |
|           user,
 | |
|           id,
 | |
|           { id, solution, completedDate, files },
 | |
|           timezone
 | |
|         );
 | |
| 
 | |
|         const points = alreadyCompleted ? user.points : user.points + 1;
 | |
| 
 | |
|         const updatePromise = new Promise((resolve, reject) =>
 | |
|           user.updateAttributes(updateData, err => {
 | |
|             if (err) {
 | |
|               return reject(err);
 | |
|             }
 | |
|             return resolve();
 | |
|           })
 | |
|         );
 | |
|         return Observable.fromPromise(updatePromise).map(() => {
 | |
|           if (type === 'json') {
 | |
|             return res.json({
 | |
|               points,
 | |
|               alreadyCompleted,
 | |
|               completedDate
 | |
|             });
 | |
|           }
 | |
|           return res.sendStatus(200);
 | |
|         });
 | |
|       })
 | |
|       .subscribe(() => {}, next);
 | |
|   }
 | |
| 
 | |
|   function projectCompleted(req, res, next) {
 | |
|     const type = accepts(req).type('html', 'json', 'text');
 | |
|     req.checkBody('id', 'id must be an ObjectId').isMongoId();
 | |
|     req.checkBody('challengeType', 'must be a number').isNumber();
 | |
|     req.checkBody('solution', 'solution must be a URL').isURL();
 | |
| 
 | |
|     const errors = req.validationErrors(true);
 | |
| 
 | |
|     if (errors) {
 | |
|       if (type === 'json') {
 | |
|         return res.status(403).send({ errors });
 | |
|       }
 | |
|       log('errors', errors);
 | |
|       return res.sendStatus(403);
 | |
|     }
 | |
| 
 | |
|     const { user, body = {} } = req;
 | |
| 
 | |
|     const completedChallenge = _.pick(body, [
 | |
|       'id',
 | |
|       'solution',
 | |
|       'githubLink',
 | |
|       'challengeType',
 | |
|       'files'
 | |
|     ]);
 | |
|     completedChallenge.completedDate = Date.now();
 | |
| 
 | |
|     if (
 | |
|       !completedChallenge.solution ||
 | |
|       // only basejumps require github links
 | |
|       (completedChallenge.challengeType === 4 && !completedChallenge.githubLink)
 | |
|     ) {
 | |
|       req.flash(
 | |
|         'danger',
 | |
|         "You haven't supplied the necessary URLs for us to inspect your work."
 | |
|       );
 | |
|       return res.sendStatus(403);
 | |
|     }
 | |
| 
 | |
|     return user
 | |
|       .getCompletedChallenges$()
 | |
|       .flatMap(() => {
 | |
|         const { alreadyCompleted, updateData } = buildUserUpdate(
 | |
|           user,
 | |
|           completedChallenge.id,
 | |
|           completedChallenge
 | |
|         );
 | |
| 
 | |
|         const updatePromise = new Promise((resolve, reject) =>
 | |
|           user.updateAttributes(updateData, err => {
 | |
|             if (err) {
 | |
|               return reject(err);
 | |
|             }
 | |
|             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);
 | |
|   }
 | |
| 
 | |
|   function backendChallengeCompleted(req, res, next) {
 | |
|     const type = accepts(req).type('html', 'json', 'text');
 | |
|     req.checkBody('id', 'id must be an ObjectId').isMongoId();
 | |
|     req.checkBody('solution', 'solution must be a URL').isURL();
 | |
| 
 | |
|     const errors = req.validationErrors(true);
 | |
| 
 | |
|     if (errors) {
 | |
|       if (type === 'json') {
 | |
|         return res.status(403).send({ errors });
 | |
|       }
 | |
|       log('errors', errors);
 | |
|       return res.sendStatus(403);
 | |
|     }
 | |
| 
 | |
|     const { user, body = {} } = req;
 | |
| 
 | |
|     const completedChallenge = _.pick(body, ['id', 'solution']);
 | |
|     completedChallenge.completedDate = Date.now();
 | |
| 
 | |
|     return user
 | |
|       .getCompletedChallenges$()
 | |
|       .flatMap(() => {
 | |
|         const { alreadyCompleted, updateData } = buildUserUpdate(
 | |
|           user,
 | |
|           completedChallenge.id,
 | |
|           completedChallenge
 | |
|         );
 | |
| 
 | |
|         const updatePromise = new Promise((resolve, reject) =>
 | |
|           user.updateAttributes(updateData, err => {
 | |
|             if (err) {
 | |
|               return reject(err);
 | |
|             }
 | |
|             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);
 | |
|   }
 | |
| 
 | |
|   async function redirectToCurrentChallenge(req, res, next) {
 | |
|     const { user } = req;
 | |
|     if (!user) {
 | |
|       return res.redirect(learnURL);
 | |
|     }
 | |
|     const challengeId = user && user.currentChallengeId;
 | |
|     log(req.user.username);
 | |
|     log(challengeId);
 | |
|     const challengeUrl = await challengeUrlResolver(challengeId).catch(next);
 | |
|     log(challengeUrl);
 | |
|     if (challengeUrl === '/learn') {
 | |
|       // this should normally not be hit if database is properly seeded
 | |
|       throw new Error(dedent`
 | |
|         Attempted to find the url for ${challengeId}'
 | |
|         but came up empty.
 | |
|         db may not be properly seeded.
 | |
|       `);
 | |
|     }
 | |
|     return res.redirect(`${homeLocation}${challengeUrl}`);
 | |
|   }
 | |
| 
 | |
|   function redirectToLearn(req, res) {
 | |
|     const maybeChallenge = _.last(req.path.split('/'));
 | |
|     if (maybeChallenge in pathMigrations) {
 | |
|       const redirectPath = pathMigrations[maybeChallenge];
 | |
|       return res.status(302).redirect(`${learnURL}${redirectPath}`);
 | |
|     }
 | |
|     return res.status(302).redirect(learnURL);
 | |
|   }
 | |
|   done();
 | |
| }
 |