They should be handled either by nginx or by the client. Turned out a lot of code, including the path migration, existed to support them. Hence the large number of removals
		
			
				
	
	
		
			352 lines
		
	
	
		
			9.2 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
			
		
		
	
	
			352 lines
		
	
	
		
			9.2 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 } 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/slugs';
 | |
| import { fixCompletedChallengeItem } from '../../common/utils';
 | |
| import { getChallenges } from '../utils/get-curriculum';
 | |
| 
 | |
| 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 challengeUrlResolver = await createChallengeUrlResolver(
 | |
|     await getChallenges()
 | |
|   );
 | |
|   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);
 | |
| 
 | |
|   app.use(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}`;
 | |
| }
 | |
| 
 | |
| // this is only called once during boot, so it can be slow.
 | |
| export function getFirstChallenge(allChallenges) {
 | |
|   const first = allChallenges.find(
 | |
|     ({ challengeOrder, superOrder, order }) =>
 | |
|       challengeOrder === 0 && superOrder === 1 && order === 0
 | |
|   );
 | |
| 
 | |
|   return first ? buildChallengeUrl(first) : '/learn';
 | |
| }
 | |
| 
 | |
| function getChallengeById(allChallenges, targetId) {
 | |
|   return allChallenges.find(({ id }) => id === targetId);
 | |
| }
 | |
| 
 | |
| export async function createChallengeUrlResolver(
 | |
|   allChallenges,
 | |
|   { _getFirstChallenge = getFirstChallenge } = {}
 | |
| ) {
 | |
|   const cache = new Map();
 | |
|   const firstChallenge = _getFirstChallenge(allChallenges);
 | |
| 
 | |
|   return function resolveChallengeUrl(id) {
 | |
|     if (isEmpty(id)) {
 | |
|       return Promise.resolve(firstChallenge);
 | |
|     } else {
 | |
|       return new Promise(resolve => {
 | |
|         if (cache.has(id)) {
 | |
|           resolve(cache.get(id));
 | |
|         }
 | |
| 
 | |
|         const challenge = getChallengeById(allChallenges, id);
 | |
|         if (isEmpty(challenge)) {
 | |
|           resolve(firstChallenge);
 | |
|         } else {
 | |
|           const challengeUrl = buildChallengeUrl(challenge);
 | |
|           cache.set(id, challengeUrl);
 | |
|           resolve(challengeUrl);
 | |
|         }
 | |
|       });
 | |
|     }
 | |
|   };
 | |
| }
 | |
| 
 | |
| export function isValidChallengeCompletion(req, res, next) {
 | |
|   const {
 | |
|     body: { id, challengeType, solution }
 | |
|   } = req;
 | |
| 
 | |
|   const isValidChallengeCompletionErrorMsg = {
 | |
|     type: 'error',
 | |
|     message: 'That does not appear to be a valid challenge submission.'
 | |
|   };
 | |
| 
 | |
|   if (!ObjectID.isValid(id)) {
 | |
|     log('isObjectId', id, ObjectID.isValid(id));
 | |
|     return res.status(403).json(isValidChallengeCompletionErrorMsg);
 | |
|   }
 | |
|   if ('challengeType' in req.body && !isNumeric(String(challengeType))) {
 | |
|     log('challengeType', challengeType, isNumeric(challengeType));
 | |
|     return res.status(403).json(isValidChallengeCompletionErrorMsg);
 | |
|   }
 | |
|   if ('solution' in req.body && !isURL(solution)) {
 | |
|     log('isObjectId', id, ObjectID.isValid(id));
 | |
|     return res.status(403).json(isValidChallengeCompletionErrorMsg);
 | |
|   }
 | |
|   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) {
 | |
|     return res.status(403).json({
 | |
|       type: 'error',
 | |
|       message:
 | |
|         'You have not provided the valid links for us to inspect your work.'
 | |
|     });
 | |
|   }
 | |
| 
 | |
|   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}`);
 | |
|   };
 | |
| }
 |