370 lines
		
	
	
		
			9.6 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
			
		
		
	
	
			370 lines
		
	
	
		
			9.6 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
/**
 | 
						|
 *
 | 
						|
 * Any ref to fixCompletedChallengesItem should be removed post
 | 
						|
 * a db migration to fix all completedChallenges
 | 
						|
 *
 | 
						|
 */
 | 
						|
import { Observable } from 'rx';
 | 
						|
import { isEmpty, pick, omit, find, uniqBy, last } from 'lodash';
 | 
						|
import debug from 'debug';
 | 
						|
import dedent from 'dedent';
 | 
						|
import { ObjectID } from 'mongodb';
 | 
						|
import isNumeric from 'validator/lib/isNumeric';
 | 
						|
import isURL from 'validator/lib/isURL';
 | 
						|
 | 
						|
import { homeLocation } from '../../../config/env';
 | 
						|
 | 
						|
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');
 | 
						|
 | 
						|
export default async function bootChallenge(app, done) {
 | 
						|
  const send200toNonUser = ifNoUserSend(true);
 | 
						|
  const api = app.loopback.Router();
 | 
						|
  const router = app.loopback.Router();
 | 
						|
  const redirectToLearn = createRedirectToLearn(_pathMigrations);
 | 
						|
  const challengeUrlResolver = await createChallengeUrlResolver(app);
 | 
						|
  const redirectToCurrentChallenge = createRedirectToCurrentChallenge(
 | 
						|
    challengeUrlResolver
 | 
						|
  );
 | 
						|
 | 
						|
  api.post(
 | 
						|
    '/modern-challenge-completed',
 | 
						|
    send200toNonUser,
 | 
						|
    isValidChallengeCompletion,
 | 
						|
    modernChallengeCompleted
 | 
						|
  );
 | 
						|
 | 
						|
  api.post(
 | 
						|
    '/project-completed',
 | 
						|
    send200toNonUser,
 | 
						|
    isValidChallengeCompletion,
 | 
						|
    projectCompleted
 | 
						|
  );
 | 
						|
 | 
						|
  api.post(
 | 
						|
    '/backend-challenge-completed',
 | 
						|
    send200toNonUser,
 | 
						|
    isValidChallengeCompletion,
 | 
						|
    backendChallengeCompleted
 | 
						|
  );
 | 
						|
 | 
						|
  router.get('/challenges/current-challenge', redirectToCurrentChallenge);
 | 
						|
 | 
						|
  router.get('/challenges', redirectToLearn);
 | 
						|
 | 
						|
  router.get('/challenges/*', redirectToLearn);
 | 
						|
 | 
						|
  router.get('/map', redirectToLearn);
 | 
						|
 | 
						|
  app.use(api);
 | 
						|
  app.use('/internal', api);
 | 
						|
  app.use(router);
 | 
						|
  done();
 | 
						|
}
 | 
						|
const learnURL = `${homeLocation}/learn`;
 | 
						|
 | 
						|
const jsProjects = [
 | 
						|
  'aaa48de84e1ecc7c742e1124',
 | 
						|
  'a7f4d8f2483413a6ce226cac',
 | 
						|
  '56533eb9ac21ba0edf2244e2',
 | 
						|
  'aff0395860f5d3034dc0bfc9',
 | 
						|
  'aa2e6f85cab2ab736c9a9b24'
 | 
						|
];
 | 
						|
 | 
						|
export 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
 | 
						|
  };
 | 
						|
}
 | 
						|
 | 
						|
export function buildChallengeUrl(challenge) {
 | 
						|
  const { superBlock, block, dashedName } = challenge;
 | 
						|
  return `/learn/${dasherize(superBlock)}/${dasherize(block)}/${dashedName}`;
 | 
						|
}
 | 
						|
 | 
						|
export function getFirstChallenge(Challenge) {
 | 
						|
  return new Promise(resolve => {
 | 
						|
    Challenge.findOne(
 | 
						|
      { where: { challengeOrder: 0, superOrder: 1, order: 0 } },
 | 
						|
      (err, challenge) => {
 | 
						|
        if (err || isEmpty(challenge)) {
 | 
						|
          return resolve('/learn');
 | 
						|
        }
 | 
						|
        return resolve(buildChallengeUrl(challenge));
 | 
						|
      }
 | 
						|
    );
 | 
						|
  });
 | 
						|
}
 | 
						|
 | 
						|
export async function createChallengeUrlResolver(
 | 
						|
  app,
 | 
						|
  { _getFirstChallenge = getFirstChallenge } = {}
 | 
						|
) {
 | 
						|
  const { Challenge } = app.models;
 | 
						|
  const cache = new Map();
 | 
						|
  const firstChallenge = await _getFirstChallenge(Challenge);
 | 
						|
  return function resolveChallengeUrl(id) {
 | 
						|
    if (isEmpty(id)) {
 | 
						|
      return Promise.resolve(firstChallenge);
 | 
						|
    }
 | 
						|
    return new Promise(resolve => {
 | 
						|
      if (cache.has(id)) {
 | 
						|
        return resolve(cache.get(id));
 | 
						|
      }
 | 
						|
      return Challenge.findById(id, (err, challenge) => {
 | 
						|
        if (err || isEmpty(challenge)) {
 | 
						|
          return resolve(firstChallenge);
 | 
						|
        }
 | 
						|
        const challengeUrl = buildChallengeUrl(challenge);
 | 
						|
        cache.set(id, challengeUrl);
 | 
						|
        return resolve(challengeUrl);
 | 
						|
      });
 | 
						|
    });
 | 
						|
  };
 | 
						|
}
 | 
						|
 | 
						|
export function isValidChallengeCompletion(req, res, next) {
 | 
						|
  const {
 | 
						|
    body: { id, challengeType, solution }
 | 
						|
  } = req;
 | 
						|
 | 
						|
  if (!ObjectID.isValid(id)) {
 | 
						|
    log('isObjectId', id, ObjectID.isValid(id));
 | 
						|
    return res.sendStatus(403);
 | 
						|
  }
 | 
						|
  if ('challengeType' in req.body && !isNumeric(String(challengeType))) {
 | 
						|
    log('challengeType', challengeType, isNumeric(challengeType));
 | 
						|
    return res.sendStatus(403);
 | 
						|
  }
 | 
						|
  if ('solution' in req.body && !isURL(solution)) {
 | 
						|
    log('isObjectId', id, ObjectID.isValid(id));
 | 
						|
    return res.sendStatus(403);
 | 
						|
  }
 | 
						|
  return next();
 | 
						|
}
 | 
						|
 | 
						|
export function modernChallengeCompleted(req, res, next) {
 | 
						|
  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(() => {
 | 
						|
        return res.json({
 | 
						|
          points,
 | 
						|
          alreadyCompleted,
 | 
						|
          completedDate
 | 
						|
        });
 | 
						|
      });
 | 
						|
    })
 | 
						|
    .subscribe(() => {}, next);
 | 
						|
}
 | 
						|
 | 
						|
function projectCompleted(req, res, next) {
 | 
						|
  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(() => {
 | 
						|
        return res.send({
 | 
						|
          alreadyCompleted,
 | 
						|
          points: alreadyCompleted ? user.points : user.points + 1,
 | 
						|
          completedDate: completedChallenge.completedDate
 | 
						|
        });
 | 
						|
      });
 | 
						|
    })
 | 
						|
    .subscribe(() => {}, next);
 | 
						|
}
 | 
						|
 | 
						|
function backendChallengeCompleted(req, res, next) {
 | 
						|
  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(() => {
 | 
						|
        return res.send({
 | 
						|
          alreadyCompleted,
 | 
						|
          points: alreadyCompleted ? user.points : user.points + 1,
 | 
						|
          completedDate: completedChallenge.completedDate
 | 
						|
        });
 | 
						|
      });
 | 
						|
    })
 | 
						|
    .subscribe(() => {}, next);
 | 
						|
}
 | 
						|
 | 
						|
export function createRedirectToCurrentChallenge(
 | 
						|
  challengeUrlResolver,
 | 
						|
  { _homeLocation = homeLocation, _learnUrl = learnURL } = {}
 | 
						|
) {
 | 
						|
  return async function redirectToCurrentChallenge(req, res, next) {
 | 
						|
    const { user } = req;
 | 
						|
    if (!user) {
 | 
						|
      return res.redirect(_learnUrl);
 | 
						|
    }
 | 
						|
    const challengeId = user && user.currentChallengeId;
 | 
						|
    const challengeUrl = await challengeUrlResolver(challengeId).catch(next);
 | 
						|
    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 || 'Unknown ID'}'
 | 
						|
        but came up empty.
 | 
						|
        db may not be properly seeded.
 | 
						|
      `);
 | 
						|
    }
 | 
						|
    return res.redirect(`${_homeLocation}${challengeUrl}`);
 | 
						|
  };
 | 
						|
}
 | 
						|
 | 
						|
export function createRedirectToLearn(
 | 
						|
  pathMigrations,
 | 
						|
  base = homeLocation,
 | 
						|
  learn = learnURL
 | 
						|
) {
 | 
						|
  return function redirectToLearn(req, res) {
 | 
						|
    const maybeChallenge = last(req.path.split('/'));
 | 
						|
    if (maybeChallenge in pathMigrations) {
 | 
						|
      const redirectPath = pathMigrations[maybeChallenge];
 | 
						|
      return res.status(302).redirect(`${base}${redirectPath}`);
 | 
						|
    }
 | 
						|
    return res.status(302).redirect(learn);
 | 
						|
  };
 | 
						|
}
 |