Data Validation
Motivation
This section outlines data validation techniques used in the project. The goal is to ensure data integrity and security. These techniques involve input validation and sanitization.
Input Validation and Sanitization
This section focuses on the techniques used to validate and sanitize user input. These techniques are implemented to mitigate potential security risks and ensure data integrity.
Filename: audience-app/database.rules.json
{
"rules": {
".read": "root.child('admins').child(auth.uid).val() != null",
".write": "root.child('admins').child(auth.uid).val() != null",
"answers": {
"$eventId": {
"$questionId": {
"void": {},
"$user_id": {
".write": "!data.exists() && newData.hasChildren(['answer','displayName','submitTime'])",
".validate": "newData.child('submitTime').isNumber() && newData.child('answer').isString() && newData.child('answer').val().length < 100 && newData.child('displayName').val().length < 100"
}
}
}
},
"activeQuestion": {
".read": true
}
}
}
The Firebase database rules specify that data must be validated before it can be written to the database. The ".validate"
rule ensures that submitTime
is a number, answer
is a string, and both answer
and displayName
are less than 100 characters long. This rule ensures that all user submissions are valid before being saved to the database.
Filename: audience-app/public/index.html
Submit
The HTML code includes a “Submit” button, which triggers a submission of user answers to the database. The submission process includes client-side validation, but the server-side rules are also implemented to ensure that only valid data is saved.
Filename: presenter-app/src/controllers/QuestionController.ts
class _QuestionController {
getAllQuestions(): Question[] {
return QUESTIONS;
}
/**
* Get a random question at the specified difficulty for the game. If it has already been
* requested, the same question is returned.
*/
getQuestion(gameId: string, questionIdx: number, difficulty: 0 | 1 | 2 | 3 | 4 | 9): Question {
let claims = this.getClaimRecords();
let claimKey = `${gameId}${questionIdx}`;
let existingClaim = claims.find(claim => claim.claimKey === claimKey);
if (existingClaim) {
return QUESTIONS.find(question => question.id === existingClaim?.questionId) as Question;
}
let availableQuestions = QUESTIONS
.filter(question => !claims.some(claim => claim.questionId === question.id)) // remove existing claims
.filter(question => question.difficulty === difficulty); // match difficulty
let question = availableQuestions[getRandomInteger(0, availableQuestions.length)];
if (!question) {
throw new Error(`No questions remaining for difficulty ${difficulty}.`);
}
claims.push({
claimKey, questionId: question.id
});
this.saveClaimRecords(claims);
return question;
}
logQuestions() {
console.groupCollapsed("Question Integrity Check");
console.info(`There are ${QUESTIONS.length} questions.`)
console.info(`${QUESTIONS.filter(q => !!QUESTIONS.find(qi => qi.id === q.id && q !== qi)).length} have duplicate ids`);
console.info(`${QUESTIONS.filter(q => q.answers.filter(a => a.id === q.correctId).length !== 1).length} have invalid answers`);
console.info(`${QUESTIONS.filter(q => q.difficulty === 0).length} have difficulty 0`);
console.info(`${QUESTIONS.filter(q => q.difficulty === 1).length} have difficulty 1`);
console.info(`${QUESTIONS.filter(q => q.difficulty === 2).length} have difficulty 2`);
console.info(`${QUESTIONS.filter(q => q.difficulty === 3).length} have difficulty 3`);
console.info(`${QUESTIONS.filter(q => q.difficulty === 4).length} have difficulty 4`);
console.info(`${QUESTIONS.filter(q => q.difficulty === 9).length} have difficulty 9`);
console.groupEnd();
}
private getClaimRecords(): QuestionClaim[] {
let usageRecords = JSON.parse(localStorage.getItem(STORAGE_KEY) || "[]");
return usageRecords;
}
private saveClaimRecords(usageRecords: QuestionClaim[]): void {
localStorage.setItem(STORAGE_KEY, JSON.stringify(usageRecords));
}
}
The QuestionController
class includes a logQuestions
method, which verifies the integrity of the question database. This check ensures that the questions are correctly formatted, avoiding issues like duplicate IDs or invalid answers.
The getQuestion
method ensures that no question is repeated during a game. The claims
array, which stores the IDs of questions that have already been used, is utilized to prevent the selection of duplicate questions.
Filename: audience-app/database.template.json
"questions": {
"text": {
"qt00": {
"questionText": "\"💩\" - \"💩\"",
"answer": "NaN",
"used": false
},
"qt01": {
"questionText": "\"💩\".length",
"answer": "2",
"used": false
},
"qt02": {
"questionText": "0.1 + 0.2",
"answer": "0.30000000000000004",
"used": false
},
"qt03": {
"questionText": "+\"42\"",
"answer": "42",
"used": false
},
"qt05": {
"questionText": "NaN == NaN",
"answer": "false",
"used": false
},
"qt06": {
"questionText": "3 > 2 > 1",
"answer": "false",
"used": false
},
"qt08": {
"questionText": "null instanceof Object",
"answer": "false",
"used": false
}
},
"choice": {
"qc00": {
"questionText": "1 / 0",
"used": false,
"answers": {
"a": {
"answerText": "Infinity",
"correct": true
},
"b": {
"answerText": "NaN",
"correct": false
},
"c": {
"answerText": "Number.MAX_VALUE",
"correct": false
},
"d": {
"answerText": "undefined",
"correct": false
}
}
},
"qc01": {
"questionText": "\"42\" + 1",
"used": false,
"answers": {
"a00": {
"answerText": "421",
"correct": true
},
"a01": {
"answerText": "43",
"correct": false
},
"a02": {
"answerText": "\"43\"",
"correct": false
},
"a03": {
"answerText": "NaN",
"correct": false
}
}
},
"qc08": {
"questionText": "\"💩\" instanceof String",
"used": false,
"answers": {
"a00": {
"answerText": "false",
"correct": true
},
"a01": {
"answerText": "true",
"correct": false
},
"a02": {
"answerText": "undefined",
"correct": false
},
"a03": {
"answerText": "0",
"correct": false
}
}
},
"qc09": {
"questionText": "false == NaN",
"used": false,
"answers": {
"a00": {
"answerText": "false",
"correct": true
},
"a01": {
"answerText": "true",
"correct": false
},
"a02": {
"answerText": "undefined",
"correct": false
},
"a03": {
"answerText": "NaN",
"correct": false
}
}
},
"qc05": {
"questionText": "[10, 5, 1].sort()",
"used": false,
"answers": {
"a00": {
"answerText": "[1, 10, 5]",
"correct": true
},
"a01": {
"answerText": "[1, 5, 10]",
"correct": false
},
"a02": {
"answerText": "[10, 5, 1]",
"correct": false
},
"a03": {
"answerText": "[10, 5, 1]",
"correct": false
}
}
},
"qc06": {
"questionText": "[1, 2, 3] + [4, 5, 6]",
"used": false,
"answers": {
"a": {
"answerText": "\"1,2,34,5,6\"",
"correct": true
},
"b": {
"answerText": "\"1,2,3,4,5,6\"",
"correct": false
},
"c": {
"answerText": "[1,2,3,4,5,6]",
"correct": false
},
"d": {
"answerText": "\"123456\"",
"correct": false
}
}
},
"qc02": {
"questionText": "[] + []",
"used": false,
"answers": {
"a": {
"answerText": "\"\"",
"correct": true
},
"b": {
"answerText": "NaN",
"correct": false
},
"c": {
"answerText": "undefined",
"correct": false
},
"d": {
"answerText": "[]",
"correct": false
}
}
},
"qc03": {
"questionText": "typeof null",
"used": false,
"answers": {
"a00": {
"answerText": "\"object\"",
"correct": true
},
"a01": {
"answerText": "\"null\"",
"correct": false
},
"a02": {
"answerText": "null",
"correct": false
},
"a03": {
"answerText": "undefined",
"correct": false
}
}
},
"qc04": {
"questionText": "parseInt(0.0000005)",
"used": false,
"answers": {
"a00": {
"answerText": "5",
"correct": true
},
"a01": {
"answerText": "undefined",
"correct": false
},
"a02": {
"answerText": "0",
"correct": false
},
"a03": {
"answerText": "ArgumentError",
"correct": false
}
}
},
"qc07": {
"questionText": "typeof typeof 1",
"used": false,
"answers": {
"a00": {
"answerText": "string",
"correct": true
},
"a01": {
"answerText": "number",
"correct": false
},
"a02": {
"answerText": "false",
"correct": false
},
"a03": {
"answerText": "undefined",
"correct": false
}
}
}
}
}
}
The question database in database.template.json
includes both “text” and “choice” type questions. The used
property is used to track which questions have already been asked during a game. The “choice” type questions ensure that at least one answer is correct per question.
Summary
These data validation techniques, both on the client and server sides, are implemented to maintain data integrity and security. By validating data, the project ensures accurate information and prevents unauthorized access to sensitive data.
Top-Level Directory Explanations
audience-app/ - This directory contains the files for the audience-facing part of the application. It includes HTML files for different pages, static assets like images, and configuration files for Firebase and npm.
audience-app/public/ - This subdirectory holds the publicly accessible files of the audience app. It includes HTML files for specific pages, images, and other static assets.
presenter-app/ - This directory contains the files for the presenter-facing part of the application. It includes source code, static assets, and configuration files.
presenter-app/build/ - This subdirectory holds the compiled and bundled files for the presenter app. It includes HTML, CSS, JavaScript, and image files.
presenter-app/src/ - This subdirectory contains the source code for the presenter app. It includes components, controllers, routes, styles, and utility functions.
presenter-app/tests/ - This subdirectory contains test files for the presenter app. It includes mocks, controllers, and declarations.