/** * * Any ref to fixCompletedChallengesItem should be removed post * a db migration to fix all completedChallenges * */ import _ from 'lodash'; import debug from 'debug'; import accepts from 'accepts'; import dedent from 'dedent'; import { ifNoUserSend } from '../utils/middleware'; import { getChallengeById, cachedMap } from '../utils/map'; import { dasherize } from '../utils'; import pathMigrations from '../resources/pathMigration.json'; import { fixCompletedChallengeItem } from '../../common/utils'; const log = debug('fcc:boot:challenges'); const learnURL = 'https://learn.freecodecamp.org'; 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 }; } log('user update data', updateData); return { alreadyCompleted, updateData, completedDate: finalChallenge.completedDate }; } export default function(app) { const send200toNonUser = ifNoUserSend(true); const api = app.loopback.Router(); const router = app.loopback.Router(); const map = cachedMap(app.models); 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; return user.update$(updateData) .doOnNext(() => user.manualReload()) .doOnNext(({ count }) => log('%s documents updated', count)) .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); if (errors) { if (type === 'json') { return res.status(403).send({ errors }); } log('errors', errors); return res.sendStatus(403); } return req.user.getCompletedChallenges$() .flatMap(() => { const completedDate = Date.now(); const { id, solution, timezone, files } = req.body; const { alreadyCompleted, updateData } = buildUserUpdate( req.user, id, { id, solution, completedDate, files }, timezone ); const user = req.user; const points = alreadyCompleted ? user.points : user.points + 1; return user.update$(updateData) .doOnNext(({ count }) => log('%s documents updated', count)) .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); return user.update$(updateData) .doOnNext(() => user.manualReload()) .doOnNext(({ count }) => log('%s documents updated', count)) .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); return user.update$(updateData) .doOnNext(({ count }) => log('%s documents updated', count)) .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 redirectToCurrentChallenge(req, res, next) { const { user } = req; const challengeId = user && user.currentChallengeId; return getChallengeById(map, challengeId) .map(challenge => { const { block, dashedName, superBlock } = challenge; if (!dashedName || !block) { // this should normally not be hit if database is properly seeded throw new Error(dedent` Attempted to find '${dashedName}' from '${ challengeId || 'no challenge id found'}' but came up empty. db may not be properly seeded. `); } return `${learnURL}/${dasherize(superBlock)}/${block}/${dashedName}`; }) .subscribe( redirect => res.redirect(redirect || learnURL), next ); } 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); } }