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();
 | 
						|
}
 |