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.