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,21 +57,31 @@ 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) {
if (res) {
window.location =
'/challenges/next-challenge?id=' + common.challengeId;
}
}); });
$.ajax({
url: '/completed-challenge/',
type: 'POST',
data,
contentType: 'application/json',
dataType: 'json'
})
.success(function(res) {
if (res) {
window.location =
'/challenges/next-challenge?id=' + common.challengeId;
}
})
.fail(function() {
window.location.replace(window.location.href);
});
}); });
}; };

View File

@ -149,38 +149,45 @@ window.common = (function({ $, common = { init: [] }}) {
e.preventDefault(); e.preventDefault();
$('#submit-challenge') $('#submit-challenge')
.attr('disabled', 'true') .attr('disabled', 'true')
.removeClass('btn-primary') .removeClass('btn-primary')
.addClass('btn-warning disabled'); .addClass('btn-warning disabled');
var $checkmarkContainer = $('#checkmark-container'); var $checkmarkContainer = $('#checkmark-container');
$checkmarkContainer.css({ height: $checkmarkContainer.innerHeight() }); $checkmarkContainer.css({ height: $checkmarkContainer.innerHeight() });
$('#challenge-checkmark') $('#challenge-checkmark')
.addClass('zoomOutUp') .addClass('zoomOutUp')
.delay(1000) .delay(1000)
.queue(function(next) { .queue(function(next) {
$(this).replaceWith( $(this).replaceWith(
'<div id="challenge-spinner" ' + '<div id="challenge-spinner" ' +
'class="animated zoomInUp inner-circles-loader">' + 'class="animated zoomInUp inner-circles-loader">' +
'submitting...</div>' 'submitting...</div>'
); );
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 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();
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 { user, body = {} } = req;
let completedChallenge; const completedChallenge = _.pick(
// backwards compatibility body,
// please remove once in production [ 'id', 'name', 'solution', 'githubLink', 'challengeType' ]
// to allow users to transition to new client code );
if (body.challengeInfo) { completedChallenge.challengeType = +completedChallenge.challengeType;
completedChallenge.completedDate = Date.now();
if (!body.challengeInfo.challengeId) {
req.flash('error', { msg: 'No id returned during save' });
return res.sendStatus(403);
}
completedChallenge = {
id: body.challengeInfo.challengeId,
name: body.challengeInfo.challengeName || '',
completedDate: Date.now(),
challengeType: +body.challengeInfo.challengeType === 4 ? 4 : 3,
solution: body.challengeInfo.publicURL,
githubLink: body.challengeInfo.githubURL
};
} else {
completedChallenge = _.pick(
body,
[ 'id', 'name', 'solution', 'githubLink', 'challengeType' ]
);
completedChallenge.challengeType = +completedChallenge.challengeType;
completedChallenge.completedDate = Date.now();
}
if ( if (
!completedChallenge.solution || !completedChallenge.solution ||
@ -603,18 +621,30 @@ module.exports = function(app) {
) { ) {
req.flash('errors', { req.flash('errors', {
msg: 'You haven\'t supplied the necessary URLs for us to inspect ' + msg: 'You haven\'t supplied the necessary URLs for us to inspect ' +
'your work.' 'your work.'
}); });
return res.sendStatus(403); return res.sendStatus(403);
} }
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() {
customValidators: { return validator({
matchRegex: function matchRegex(param, regex) { customValidators: {
return regex.test(param); matchRegex(param, regex) {
return regex.test(param);
},
isString(value) {
return typeof value === 'string';
},
isNumber(value) {
return typeof value === 'number';
}
} }
} });
}); }