Add validation to challenge completion

Change ajax requests to send and accept JSON
to preserve data types.
Fix typos
This commit is contained in:
Berkeley Martinez
2016-02-10 22:10:06 -08:00
parent d8ad4a59eb
commit 6642dd497f
5 changed files with 138 additions and 71 deletions

View File

@ -81,9 +81,15 @@ window.common = (function(global) {
data = { data = {
id: common.challengeId, id: common.challengeId,
name: common.challengeName, name: common.challengeName,
challengeType: common.challengeType challengeType: +common.challengeType
}; };
$.post('/completed-challenge/', data) $.ajax({
url: '/completed-challenge/',
type: 'POST',
data: JSON.stringify(data),
contentType: 'application/json',
dataType: 'json'
})
.success(function(res) { .success(function(res) {
if (!res) { if (!res) {
return; return;
@ -92,7 +98,7 @@ window.common = (function(global) {
common.challengeId; common.challengeId;
}) })
.fail(function() { .fail(function() {
window.location.href = '/challenges'; window.location.replace(window.location.href);
}); });
break; break;
@ -106,7 +112,13 @@ window.common = (function(global) {
githubLink githubLink
}; };
$.post('/completed-zipline-or-basejump/', data) $.ajax({
url: '/completed-zipline-or-basejump/',
type: 'POST',
data: JSON.stringify(data),
contentType: 'application/json',
dataType: 'json'
})
.success(function() { .success(function() {
window.location.href = '/challenges/next-challenge?id=' + window.location.href = '/challenges/next-challenge?id=' +
common.challengeId; common.challengeId;

View File

@ -57,20 +57,30 @@ window.common = (function(global) {
`; `;
console.error(err); console.error(err);
} }
const data = { const data = JSON.stringify({
id: common.challengeId, id: common.challengeId,
name: common.challengeName, name: common.challengeName,
completedWith: didCompleteWith, completedWith: didCompleteWith,
challengeType: common.challengeType, challengeType: +common.challengeType,
solution, solution,
timezone timezone
}; });
$.post('/completed-challenge/', data, function(res) { $.ajax({
url: '/completed-challenge/',
type: 'POST',
data,
contentType: 'application/json',
dataType: 'json'
})
.success(function(res) {
if (res) { if (res) {
window.location = window.location =
'/challenges/next-challenge?id=' + common.challengeId; '/challenges/next-challenge?id=' + common.challengeId;
} }
})
.fail(function() {
window.location.replace(window.location.href);
}); });
}); });
}; };

View File

@ -168,19 +168,26 @@ window.common = (function({ $, common = { init: [] }}) {
next(); next();
}); });
$.post( $.ajax({
'/completed-challenge/', { url: '/completed-challenge/',
type: 'POST',
data: JSON.stringify({
id: common.challengeId, id: common.challengeId,
name: common.challengeName, name: common.challengeName,
challengeType: common.challengeType challengeType: +common.challengeType
}, }),
function(res) { contentType: 'application/json',
dataType: 'json'
})
.success(function(res) {
if (res) { if (res) {
window.location = window.location =
'/challenges/next-challenge?id=' + common.challengeId; '/challenges/next-challenge?id=' + common.challengeId;
} }
} })
); .fail(function() {
window.location.replace(window.location.href);
});
} }
common.init.push(function($) { common.init.push(function($) {

View File

@ -515,8 +515,28 @@ module.exports = function(app) {
} }
function completedChallenge(req, res, next) { function completedChallenge(req, res, next) {
req.checkBody('id', 'id must be a ObjectId').isMongoId();
req.checkBody('name', 'name must be at least 3 characters')
.isString()
.isLength({ min: 3 });
req.checkBody('challengeType', 'challengeType must be an integer')
.isNumber()
.isInt();
const type = accepts(req).type('html', 'json', 'text'); 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);
}
const completedDate = Date.now(); const completedDate = Date.now();
const { const {
id, id,
@ -543,6 +563,7 @@ module.exports = function(app) {
const points = alreadyCompleted ? const points = alreadyCompleted ?
user.progressTimestamps.length : user.progressTimestamps.length :
user.progressTimestamps.length + 1; user.progressTimestamps.length + 1;
return user.update$(updateData) return user.update$(updateData)
.doOnNext(({ count }) => log('%s documents updated', count)) .doOnNext(({ count }) => log('%s documents updated', count))
.subscribe( .subscribe(
@ -561,37 +582,34 @@ module.exports = function(app) {
} }
function completedZiplineOrBasejump(req, res, next) { function completedZiplineOrBasejump(req, res, next) {
const { user, body = {} } = req; const type = accepts(req).type('html', 'json', 'text');
req.checkBody('id', 'id must be an ObjectId').isMongoId();
req.checkBody('name', 'Name must be at least 3 characters')
.isString()
.isLength({ min: 3 });
req.checkBody('challengeType', 'must be a number')
.isNumber()
.isInt();
req.checkBody('solution', 'solution must be a url').isURL();
let completedChallenge; const errors = req.validationErrors(true);
// backwards compatibility
// please remove once in production
// to allow users to transition to new client code
if (body.challengeInfo) {
if (!body.challengeInfo.challengeId) { if (errors) {
req.flash('error', { msg: 'No id returned during save' }); if (type === 'json') {
return res.status(403).send({ errors });
}
log('errors', errors);
return res.sendStatus(403); return res.sendStatus(403);
} }
completedChallenge = { const { user, body = {} } = req;
id: body.challengeInfo.challengeId,
name: body.challengeInfo.challengeName || '',
completedDate: Date.now(),
challengeType: +body.challengeInfo.challengeType === 4 ? 4 : 3, const completedChallenge = _.pick(
solution: body.challengeInfo.publicURL,
githubLink: body.challengeInfo.githubURL
};
} else {
completedChallenge = _.pick(
body, body,
[ 'id', 'name', 'solution', 'githubLink', 'challengeType' ] [ 'id', 'name', 'solution', 'githubLink', 'challengeType' ]
); );
completedChallenge.challengeType = +completedChallenge.challengeType; completedChallenge.challengeType = +completedChallenge.challengeType;
completedChallenge.completedDate = Date.now(); completedChallenge.completedDate = Date.now();
}
if ( if (
!completedChallenge.solution || !completedChallenge.solution ||
@ -610,11 +628,23 @@ module.exports = function(app) {
const { const {
alreadyCompleted,
updateData updateData
} = buildUserUpdate(req.user, completedChallenge.id, completedChallenge); } = buildUserUpdate(req.user, completedChallenge.id, completedChallenge);
return user.updateTo$(updateData) return user.update$(updateData)
.doOnNext(() => res.status(200).send(true)) .doOnNext(({ count }) => log('%s documents updated', count))
.doOnNext(() => {
if (type === 'json') {
return res.send({
alreadyCompleted,
points: alreadyCompleted ?
user.progressTimestamps.length :
user.progressTimestamps.length + 1
});
}
res.status(200).send(true);
})
.subscribe(() => {}, next); .subscribe(() => {}, next);
} }

View File

@ -1,9 +1,17 @@
import validator from 'express-validator'; import validator from 'express-validator';
export default validator.bind(validator, { export default function() {
return validator({
customValidators: { customValidators: {
matchRegex: function matchRegex(param, regex) { matchRegex(param, regex) {
return regex.test(param); return regex.test(param);
},
isString(value) {
return typeof value === 'string';
},
isNumber(value) {
return typeof value === 'number';
} }
} }
}); });
}